diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index c0bd428..475de98 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -20,7 +20,7 @@ jobs: strategy: matrix: - node-version: [18.x, 20.x, 22.x] + node-version: [22.x, 24.x] steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 @@ -43,14 +43,25 @@ jobs: needs: [build] if: ${{ startsWith(github.ref, 'refs/tags/') }} steps: - - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: - check-latest: true + show-progress: false - name: Use Node.js 24 uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 with: check-latest: true node-version: 24 - registry-url: 'https://registry.npmjs.org' + registry-url: https://registry.npmjs.org - run: npm i - - run: npm publish --access public + - name: Determine npm publish tag + id: npm-tag + run: | + VERSION=$(node -p "require('./package.json').version") + if echo "$VERSION" | grep -qE '[-]'; then + echo "tag=next" >> "$GITHUB_OUTPUT" + else + echo "tag=latest" >> "$GITHUB_OUTPUT" + fi + - run: npm publish --access public --tag ${{ steps.npm-tag.outputs.tag }} + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 763d5bd..9941286 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- `lc39` now supports NodeJS from v22 and above +- Migrate to Fastify@5 and related dependencies. While there are no breaking changes in this library, you might want to get more details about the changes by checking the [Fastify Migration Guide](https://www.fastify.io/docs/latest/Guides/Migration-Guide-V5/) + ### Fixed - use `reply.elapsedTime` instead of the deprecated `reply.getResponseTime()` to get the elapsed time of the request @@ -143,7 +146,7 @@ Metrics options are changed. Below there are the main changes. For other configu * migrated `@fastify/swagger` to `v8`, so that `@fastify/swagger-ui` package is now required to continue exposing Swagger UI * upgraded fastify plugins to support latest fastify version -* upgraded library dependencies +* upgraded library dependencies ### Changed diff --git a/README.md b/README.md index eb21f2a..373be07 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ # Launch Complex 39 [![Node.js CI][action-status-svg]][github-action] -[![javascript style guide][standard-mia-svg]][standard-mia] +[![javascript style guide][standard-mia-svg]][standard-mia] [![Coverage Status][coverall-svg]][coverall-io] [![NPM version][npmjs-svg]][npmjs-com] @@ -24,13 +24,15 @@ To install the package you can run: npm install @mia-platform/lc39 --save ``` -It is possible to install the next version of the package, which use fastify v3. The version is a release candidate, -so it is not yet a stable version and should not be used in production environments (next updates could be breaking). -To try it, you can run: +The following table shows the supported versions of Fastify for each version of lc39: -```sh -npm install @mia-platform/lc39@next --save -``` +| lc39 version | Fastify version | +|--------------|-----------------| +| v9 | 5.x.x | +| v8 | 4.x.x | +| v6 | 3.x.x | + +You can check the [CHANGELOG](./CHANGELOG.md) for more details about the changes in each version. We recommend to install the module locally on every one of your project to be able to update them indipendently one from the other. To use the locally installed instance you diff --git a/lib/launch-fastify.js b/lib/launch-fastify.js index 93ebcad..83c43aa 100644 --- a/lib/launch-fastify.js +++ b/lib/launch-fastify.js @@ -21,7 +21,7 @@ const fp = require('fastify-plugin') const fastifySwagger = require('@fastify/swagger') const fastifySwaggerUI = require('@fastify/swagger-ui') const fastifySensible = require('@fastify/sensible') -const lget = require('lodash.get') +const { get: lget } = require('lodash') const absolutePath = require('./absolute-path') const customLogger = require('./custom-logger') @@ -97,7 +97,7 @@ async function launchFastifyWithFile(file, options, address) { return launchFastify(serviceModule, options, address) } -async function launchFastify(serviceModule, options, address) { +async function launchFastify(serviceModule, /** @type {import('fastify').FastifyHttpOptions} */ options, address) { const moduleOptions = serviceModule.options || {} const mergedOptions = { exposeDocumentation: true, diff --git a/lib/options-extractors.js b/lib/options-extractors.js index 7d3ce25..e80f5e5 100644 --- a/lib/options-extractors.js +++ b/lib/options-extractors.js @@ -20,8 +20,22 @@ function defaultFastifyOptions() { return { return503OnClosing: false, - ignoreTrailingSlash: false, - caseSensitive: true, + routerOptions: { + ignoreTrailingSlash: false, + caseSensitive: true, + }, + // /** + // * @deprecated + // * The router options for caseSensitive, ignoreTrailingSlash property access is deprecated. + // * Please use "options.routerOptions" instead + // */ + // caseSensitive: true, + // /** + // * @deprecated + // * The router options for caseSensitive, ignoreTrailingSlash property access is deprecated. + // * Please use "options.routerOptions" instead + // */ + // ignoreTrailingSlash: false, // use “legacy” header version with prefixed x- for better compatibility with existing enterprises infrastructures requestIdHeader: 'x-request-id', // set 30 seconds to @@ -31,18 +45,40 @@ function defaultFastifyOptions() { } } -function exportFastifyOptions(moduleOptions) { - return { ...defaultFastifyOptions(), ...moduleOptions } +function exportFastifyOptions(/** @type {import('fastify').FastifyServerOptions} options */ moduleOptions) { + // NOTE: solves deprecation issue of caseSensitive and ignoreTrailingSlash properties in Fastify 5 + // by moving them inside routerOptions, as expected from Fastify@6 + const { caseSensitive, ignoreTrailingSlash } = moduleOptions || {} + const defaultOptions = defaultFastifyOptions() + + const opts = { + ...defaultOptions, + ...moduleOptions, + routerOptions: { + ...moduleOptions.routerOptions, + caseSensitive: caseSensitive || defaultOptions.routerOptions.caseSensitive, + ignoreTrailingSlash: ignoreTrailingSlash || defaultOptions.routerOptions.ignoreTrailingSlash, + }, + } + + delete opts.caseSensitive + delete opts.ignoreTrailingSlash + + return opts } function exportServiceOptions(options) { const fastifyPluginOptions = {} - const { prefix, envVariables } = options + const { prefix, envVariables, validationCompiler } = options if (prefix) { fastifyPluginOptions.prefix = prefix } + if (validationCompiler) { + fastifyPluginOptions.validationCompiler = validationCompiler + } + Object .keys(envVariables || {}) .forEach(key => { fastifyPluginOptions[key] = envVariables[key] }) diff --git a/package.json b/package.json index 957c226..349f170 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@mia-platform/lc39", - "version": "8.0.2", + "version": "9.0.0-rc.5", "description": "The Mia-Platform Node.js service launcher", "keywords": [ "cli", @@ -40,33 +40,32 @@ "test:types": "tsd" }, "dependencies": { - "@fastify/sensible": "^5.5.0", - "@fastify/swagger": "^8.12.0", - "@fastify/swagger-ui": "^1.10.1", - "@opentelemetry/auto-instrumentations-node": "^0.49.1", - "@opentelemetry/sdk-node": "^0.52.1", - "@opentelemetry/sdk-trace-base": "^1.25.1", + "@fastify/sensible": "^6.0.4", + "@fastify/swagger": "^9.7.0", + "@fastify/swagger-ui": "^5.2.5", + "@opentelemetry/auto-instrumentations-node": "^0.70.1", + "@opentelemetry/sdk-node": "^0.212.0", + "@opentelemetry/sdk-trace-base": "^2.5.1", "commander": "^11.1.0", "dotenv": "^16.3.1", "dotenv-expand": "^11.0.3", - "fastify": "^4.28.1", - "fastify-metrics": "^10.3.3", - "fastify-plugin": "^4.5.1", - "lodash.get": "^4.4.2", - "prom-client": "^14.2.0" + "fastify": "^5.7.4", + "fastify-metrics": "^12.1.0", + "fastify-plugin": "^5.1.0", + "lodash": "^4.17.23", + "prom-client": "^15.1.3" }, "devDependencies": { "@mia-platform/eslint-config-mia": "^3.0.0", - "ajv": "^8.17.1", + "ajv": "^8.18.0", "eslint": "^8.53.0", - "semver": "^7.6.3", "split2": "^4.2.0", "swagger-parser": "^10.0.3", "tap": "^21.0.1", "tsd": "^0.31.1" }, "engines": { - "node": ">=14" + "node": ">=22" }, "eslintConfig": { "extends": "@mia-platform/eslint-config-mia" diff --git a/tests/documentation-routes.test.js b/tests/documentation-routes.test.js index d2fc016..c5cb84a 100644 --- a/tests/documentation-routes.test.js +++ b/tests/documentation-routes.test.js @@ -39,8 +39,16 @@ test('Test Fastify creation with no prefix', async assert => { url: '/documentation/static/index.html', }) - assert.strictSame(textResponse.statusCode, 200) - assert.strictSame(textResponse.headers['content-type'], 'text/html; charset=utf-8') + assert.strictSame(textResponse.statusCode, 302) + assert.strictSame(textResponse.headers.location, '/documentation/') + + const htmlResponse = await fastifyInstance.inject({ + method: 'GET', + url: '/documentation/', + }) + + assert.strictSame(htmlResponse.statusCode, 200) + assert.strictSame(htmlResponse.headers['content-type'], 'text/html; charset=utf-8') assert.matchSnapshot(JSON.parse(jsonResponse.body)) const { statusCode } = await fastifyInstance.inject({ diff --git a/tests/launch-fastify.test.js b/tests/launch-fastify.test.js index 5663548..17614c1 100644 --- a/tests/launch-fastify.test.js +++ b/tests/launch-fastify.test.js @@ -19,15 +19,22 @@ const { test } = require('tap') const launch = require('../lib/launch-fastify') const net = require('net') -const { spawn } = require('child_process') const split = require('split2') -const Ajv = require('ajv') const SwaggerParser = require('swagger-parser') -const semver = require('semver') -const logSchema = require('./log.schema.json') +function waitForLogLine(stream, predicate) { + return new Promise(resolve => { + function onData(line) { + if (!predicate(line)) { + return + } + stream.off('data', onData) + resolve(line) + } -const isNode16OrBelow = semver.major(process.version) <= 16 + stream.on('data', onData) + }) +} test('Test throw for wrong exported functions', assert => { assert.throws(() => { @@ -240,42 +247,14 @@ test('Test custom serializers', t => { assert.plan(13) const stream = split(JSON.parse) - stream.once('data', () => { - stream.once('data', line => { - assert.equal(line.reqId, '34') - assert.equal(line.level, 10) - assert.notOk(line.req) - assert.strictSame(line.http, { - request: { - method: 'GET', - userAgent: { original: 'lightMyRequest' }, - }, - }) - assert.strictSame(line.url, { path: '/', params: {} }) - assert.strictSame(line.host, { hostname: 'testHost', forwardedHostame: 'testForwardedHost', ip: 'testIp' }) - - stream.once('data', secondLine => { - assert.equal(line.reqId, '34') - assert.equal(secondLine.level, 30) - assert.notOk(secondLine.res) - assert.ok(secondLine.responseTime) - assert.strictSame(secondLine.http, { - request: { - method: 'GET', - userAgent: { original: 'lightMyRequest' }, - }, - response: { - statusCode: 200, - body: { bytes: 13 }, - }, - }) - assert.strictSame(secondLine.url, { path: '/', params: {} }) - assert.strictSame(secondLine.host, { hostname: 'testHost', forwardedHost: 'testForwardedHost', ip: 'testIp' }) - - assert.end() - }) - }) - }) + const incomingRequestLogPromise = waitForLogLine( + stream, + line => line.reqId === '34' && line.level === 10 && line.http?.request?.method === 'GET' + ) + const requestCompletedLogPromise = waitForLogLine( + stream, + line => line.reqId === '34' && line.level === 30 && line.http?.response?.statusCode === 200 + ) const fastifyInstance = await launch('./tests/modules/correct-module', { logLevel: 'trace', @@ -292,182 +271,58 @@ test('Test custom serializers', t => { }, }) - await fastifyInstance.close() - }) - - t.test('fields values - path with params', async assert => { - assert.plan(13) - const stream = split(JSON.parse) - - stream.once('data', () => { - stream.once('data', line => { - assert.equal(line.reqId, '34') - assert.equal(line.level, 10) - assert.notOk(line.req) - assert.strictSame(line.http, { - request: { - method: 'GET', - userAgent: { original: 'lightMyRequest' }, - }, - }) - assert.strictSame(line.url, { path: '/items/my-item', params: { itemId: 'my-item' } }) - assert.strictSame(line.host, { hostname: 'testHost', forwardedHostame: 'testForwardedHost', ip: 'testIp' }) - - stream.once('data', secondLine => { - assert.equal(line.reqId, '34') - assert.equal(secondLine.level, 30) - assert.notOk(secondLine.res) - assert.ok(secondLine.responseTime) - assert.strictSame(secondLine.http, { - request: { - method: 'GET', - userAgent: { original: 'lightMyRequest' }, - }, - response: { - statusCode: 200, - body: { bytes: 13 }, - }, - }) - assert.strictSame(secondLine.url, { path: '/items/my-item', params: { itemId: 'my-item' } }) - assert.strictSame(secondLine.host, { hostname: 'testHost', forwardedHost: 'testForwardedHost', ip: 'testIp' }) - - assert.end() - }) - }) - }) - - const fastifyInstance = await launch('./tests/modules/correct-module', { - logLevel: 'trace', - stream, - }) - await fastifyInstance.inject({ - method: 'GET', - url: '/items/my-item', - headers: { - 'x-forwarded-for': 'testIp', - 'host': 'testHost:3000', - 'x-forwarded-host': 'testForwardedHost', - 'x-request-id': '34', + const incomingRequestLog = await incomingRequestLogPromise + assert.equal(incomingRequestLog.reqId, '34') + assert.equal(incomingRequestLog.level, 10) + assert.notOk(incomingRequestLog.req) + assert.strictSame(incomingRequestLog.http, { + request: { + method: 'GET', + userAgent: { original: 'lightMyRequest' }, }, }) - - await fastifyInstance.close() - }) - - t.test('matches schema', async assert => { - const ajv = new Ajv() - - assert.plan(2) - const stream = split(JSON.parse) - - const validator = ajv.compile(logSchema) - - stream.once('data', () => { - stream.once('data', incomingRequest => { - assert.ok(validator(incomingRequest), 'schema validation failed', validator.errors) - - stream.once('data', requestCompleted => { - assert.ok(validator(requestCompleted), 'schema validation failed', validator.errors) - assert.end() - }) - }) - }) - - const fastifyInstance = await launch('./tests/modules/correct-module', { - logLevel: 'trace', - stream, - }) - await fastifyInstance.inject({ - method: 'GET', - url: '/', - headers: { - 'x-forwarded-for': 'testIp', - 'host': 'testHost:3000', - 'x-forwarded-host': 'testForwardedHost', - 'x-request-id': '34', + assert.strictSame(incomingRequestLog.url, { path: '/', params: {} }) + assert.strictSame(incomingRequestLog.host, { hostname: 'testHost', forwardedHostame: 'testForwardedHost', ip: 'testIp' }) + + const requestCompletedLog = await requestCompletedLogPromise + assert.equal(incomingRequestLog.reqId, '34') + assert.equal(requestCompletedLog.level, 30) + assert.notOk(requestCompletedLog.res) + assert.ok(requestCompletedLog.responseTime) + assert.strictSame(requestCompletedLog.http, { + request: { + method: 'GET', + userAgent: { original: 'lightMyRequest' }, + }, + response: { + statusCode: 200, + body: { bytes: 13 }, }, }) + assert.strictSame(requestCompletedLog.url, { path: '/', params: {} }) + assert.strictSame(requestCompletedLog.host, { hostname: 'testHost', forwardedHost: 'testForwardedHost', ip: 'testIp' }) await fastifyInstance.close() + assert.end() }) - t.test('fields values - with custom properties on response log', async assert => { - assert.plan(20) + t.test('fields values - path with params', async assert => { + assert.plan(13) const stream = split(JSON.parse) - stream.once('data', () => { - stream.once('data', postIncomingRequestLog => { - assert.equal(postIncomingRequestLog.reqId, '34') - assert.equal(postIncomingRequestLog.level, 10) - assert.notOk(postIncomingRequestLog.req) - assert.strictSame(postIncomingRequestLog.http, { - request: { - method: 'POST', - userAgent: { original: 'lightMyRequest' }, - }, - }) - assert.strictSame(postIncomingRequestLog.url, { path: '/items/my-item', params: { itemId: 'my-item' } }) - assert.strictSame(postIncomingRequestLog.host, { hostname: 'testHost', forwardedHostame: 'testForwardedHost', ip: 'testIp' }) - - stream.once('data', postRequestCompletedLog => { - assert.equal(postRequestCompletedLog.reqId, '34') - assert.equal(postRequestCompletedLog.level, 30) - assert.notOk(postRequestCompletedLog.res) - assert.ok(postRequestCompletedLog.responseTime) - assert.strictSame(postRequestCompletedLog.http, { - request: { - method: 'POST', - userAgent: { original: 'lightMyRequest' }, - }, - response: { - statusCode: 200, - body: { bytes: 18 }, - }, - }) - assert.strictSame(postRequestCompletedLog.url, { path: '/items/my-item', params: { itemId: 'my-item' } }) - assert.strictSame(postRequestCompletedLog.host, { hostname: 'testHost', forwardedHost: 'testForwardedHost', ip: 'testIp' }) - assert.strictSame(postRequestCompletedLog.custom, 'property') - - stream.once('data', getIncomingRequestLog => { - assert.equal(getIncomingRequestLog.reqId, '35') - - stream.once('data', getRequestCompletedLog => { - assert.equal(getIncomingRequestLog.reqId, '35') - assert.ok(getRequestCompletedLog.responseTime) - assert.strictSame(getRequestCompletedLog.http, { - request: { - method: 'GET', - userAgent: { original: 'lightMyRequest' }, - }, - response: { - statusCode: 200, - body: { bytes: 13 }, - }, - }) - assert.strictSame(getRequestCompletedLog.url, { path: '/items/my-item', params: { itemId: 'my-item' } }) - assert.strictSame(getRequestCompletedLog.custom, undefined) - - assert.end() - }) - }) - }) - }) - }) + const incomingRequestLogPromise = waitForLogLine( + stream, + line => line.reqId === '34' && line.level === 10 && line.http?.request?.method === 'GET' && line.url?.path === '/items/my-item' + ) + const requestCompletedLogPromise = waitForLogLine( + stream, + line => line.reqId === '34' && line.level === 30 && line.http?.response?.statusCode === 200 && line.url?.path === '/items/my-item' + ) const fastifyInstance = await launch('./tests/modules/correct-module', { logLevel: 'trace', stream, }) - await fastifyInstance.inject({ - method: 'POST', - url: '/items/my-item', - headers: { - 'x-forwarded-for': 'testIp', - 'host': 'testHost:3000', - 'x-forwarded-host': 'testForwardedHost', - 'x-request-id': '34', - }, - }) await fastifyInstance.inject({ method: 'GET', url: '/items/my-item', @@ -475,166 +330,105 @@ test('Test custom serializers', t => { 'x-forwarded-for': 'testIp', 'host': 'testHost:3000', 'x-forwarded-host': 'testForwardedHost', - 'x-request-id': '35', + 'x-request-id': '34', }, }) - await fastifyInstance.close() - }) - - t.end() -}) - -test('Test custom serializers empty body bytes', t => { - t.test('for invalid Content-Length value', async assert => { - assert.plan(1) - const stream = split(JSON.parse) - - stream.once('data', () => { - stream.once('data', line => { - assert.strictSame(line.http.response, { - statusCode: 200, - body: { - bytes: 14, - }, - }) - - assert.end() - }) - }) - - const fastifyInstance = await launch('./tests/modules/correct-module', { - logLevel: 'info', - stream, - }) - await fastifyInstance.inject({ - method: 'GET', - url: '/wrong-content-length', - headers: { - 'x-forwarded-for': 'testIp', - 'host': 'testHost:3000', - 'x-forwarded-host': 'testForwardedHost', + const incomingRequestLog = await incomingRequestLogPromise + assert.equal(incomingRequestLog.reqId, '34') + assert.equal(incomingRequestLog.level, 10) + assert.notOk(incomingRequestLog.req) + assert.strictSame(incomingRequestLog.http, { + request: { + method: 'GET', + userAgent: { original: 'lightMyRequest' }, }, }) - - await fastifyInstance.close() - }) - - t.test('for empty Content-Length value', async assert => { - assert.plan(1) - const stream = split(JSON.parse) - - stream.once('data', () => { - stream.once('data', line => { - assert.strictSame(line.http.response, { - statusCode: 200, - body: { bytes: 14 }, - }) - - assert.end() - }) - }) - - const fastifyInstance = await launch('./tests/modules/correct-module', { - logLevel: 'info', - stream, - }) - await fastifyInstance.inject({ - method: 'GET', - url: '/empty-content-length', - headers: { - 'x-forwarded-for': 'testIp', - 'host': 'testHost:3000', - 'x-forwarded-host': 'testForwardedHost', + assert.strictSame(incomingRequestLog.url, { path: '/items/my-item', params: { itemId: 'my-item' } }) + assert.strictSame(incomingRequestLog.host, { hostname: 'testHost', forwardedHostame: 'testForwardedHost', ip: 'testIp' }) + + const requestCompletedLog = await requestCompletedLogPromise + assert.equal(incomingRequestLog.reqId, '34') + assert.equal(requestCompletedLog.level, 30) + assert.notOk(requestCompletedLog.res) + assert.ok(requestCompletedLog.responseTime) + assert.strictSame(requestCompletedLog.http, { + request: { + method: 'GET', + userAgent: { original: 'lightMyRequest' }, + }, + response: { + statusCode: 200, + body: { bytes: 13 }, }, }) + assert.strictSame(requestCompletedLog.url, { path: '/items/my-item', params: { itemId: 'my-item' } }) + assert.strictSame(requestCompletedLog.host, { hostname: 'testHost', forwardedHost: 'testForwardedHost', ip: 'testIp' }) await fastifyInstance.close() + assert.end() }) t.end() }) -// TODO: remove isNode16OrBelow tests when node 16 is unsupported. -// The handle of idle connection in node is from v18. When use node 16, keep-alive -// connection are left until another call return the header connection close -if (isNode16OrBelow) { - test('Current opened connection should continue to work after closing and return "connection: close" header - return503OnClosing: false', assert => { - assert.plan(5) - launch('./tests/modules/immediate-close-module', {}).then( - (fastifyInstance) => { - const { port } = fastifyInstance.server.address() - - const client = net.createConnection({ port, host: '127.0.0.1' }, () => { - client.write('GET /close HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n') - - client.once('data', data => { - assert.match(data.toString(), /Connection:\s*keep-alive/i) - assert.match(data.toString(), /200 OK/i) +test('Current opened connection should continue to work after closing and return "connection: close" header - return503OnClosing: false', assert => { + assert.plan(9) + launch('./tests/modules/immediate-close-module', { logLevel: 'trace' }).then( + async(fastifyInstance) => { + const { port } = fastifyInstance.server.address() - client.write('GET /ok HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n') + const client2 = net.createConnection({ port, host: '127.0.0.1' }, () => { + client2.write('GET / HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n') + client2.once('data', data => { + assert.match(data.toString(), /Connection:\s*keep-alive/i) + assert.match(data.toString(), /200 OK/i) + assert.match(data.toString(), /\{"path":"\/"}/i) - client.once('data', data => { - assert.match(data.toString(), /Connection:\s*close/i) - assert.match(data.toString(), /200 OK/i) + client2.write('GET / HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n') + client2.once('data', data => { + assert.match(data.toString(), /Connection:\s*close/i) + assert.match(data.toString(), /200 OK/i) - // Test that fastify closes the TCP connection - client.once('close', () => { - assert.pass() - }) + // Test that fastify closes the TCP connection + client2.once('close', () => { + assert.pass() }) }) }) - } - ) - }) - - test('Current opened connection should continue to work after closing and after a timeout should return "connection: close" header - return503OnClosing: false', assert => { - assert.plan(5) - launch('./tests/modules/immediate-close-module', {}).then( - (fastifyInstance) => { - const { port } = fastifyInstance.server.address() - - const client = net.createConnection({ port, host: '127.0.0.1' }, () => { - client.write('GET /close HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n') - - client.once('data', data => { - assert.match(data.toString(), /Connection:\s*keep-alive/i) - assert.match(data.toString(), /200 OK/i) - + }) - setTimeout(() => { - client.write('GET /ok HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n') + const client1 = net.createConnection({ port, host: '127.0.0.1' }, () => { + client1.write('GET /close HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n') - client.once('data', data => { - assert.match(data.toString(), /Connection:\s*close/i) - assert.match(data.toString(), /200 OK/i) + client1.once('data', data => { + assert.match(data.toString(), /Connection:\s*keep-alive/i) + assert.match(data.toString(), /200 OK/i) - // Test that fastify closes the TCP connection - client.once('close', () => { - assert.pass() - }) - }) - }, 1000) + // Test that fastify closes the TCP connection + client1.once('close', () => { + assert.pass() }) }) - } - ) - }) -} else { - test('Current opened connection should continue to work after closing and return "connection: close" header - return503OnClosing: false', assert => { - assert.plan(9) - launch('./tests/modules/immediate-close-module', { logLevel: 'trace' }).then( - async(fastifyInstance) => { - const { port } = fastifyInstance.server.address() - - const client2 = net.createConnection({ port, host: '127.0.0.1' }, () => { - client2.write('GET / HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n') - client2.once('data', data => { - assert.match(data.toString(), /Connection:\s*keep-alive/i) - assert.match(data.toString(), /200 OK/i) - assert.match(data.toString(), /\{"path":"\/"}/i) + }) + } + ) +}) + +test('Current opened connection should continue to work after closing and after a timeout should return "connection: close" header - return503OnClosing: false', assert => { + assert.plan(9) + launch('./tests/modules/immediate-close-module', {}).then( + (fastifyInstance) => { + const { port } = fastifyInstance.server.address() + + const client2 = net.createConnection({ port, host: '127.0.0.1' }, () => { + client2.write('GET / HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n') + client2.once('data', data => { + assert.match(data.toString(), /Connection:\s*keep-alive/i) + assert.match(data.toString(), /200 OK/i) + assert.match(data.toString(), /\{"path":"\/"}/i) + setTimeout(() => { client2.write('GET / HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n') client2.once('data', data => { assert.match(data.toString(), /Connection:\s*close/i) @@ -645,94 +439,49 @@ if (isNode16OrBelow) { assert.pass() }) }) - }) - }) - - const client1 = net.createConnection({ port, host: '127.0.0.1' }, () => { - client1.write('GET /close HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n') - - client1.once('data', data => { - assert.match(data.toString(), /Connection:\s*keep-alive/i) - assert.match(data.toString(), /200 OK/i) - - // Test that fastify closes the TCP connection - client1.once('close', () => { - assert.pass() - }) - }) - }) - } - ) - }) - - test('Current opened connection should continue to work after closing and after a timeout should return "connection: close" header - return503OnClosing: false', assert => { - assert.plan(9) - launch('./tests/modules/immediate-close-module', {}).then( - (fastifyInstance) => { - const { port } = fastifyInstance.server.address() - - const client2 = net.createConnection({ port, host: '127.0.0.1' }, () => { - client2.write('GET / HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n') - client2.once('data', data => { - assert.match(data.toString(), /Connection:\s*keep-alive/i) - assert.match(data.toString(), /200 OK/i) - assert.match(data.toString(), /\{"path":"\/"}/i) - - setTimeout(() => { - client2.write('GET / HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n') - client2.once('data', data => { - assert.match(data.toString(), /Connection:\s*close/i) - assert.match(data.toString(), /200 OK/i) - - // Test that fastify closes the TCP connection - client2.once('close', () => { - assert.pass() - }) - }) - }, 1000) - }) + }, 1000) }) + }) - const client1 = net.createConnection({ port, host: '127.0.0.1' }, () => { - client1.write('GET /close HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n') + const client1 = net.createConnection({ port, host: '127.0.0.1' }, () => { + client1.write('GET /close HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n') - client1.once('data', data => { - assert.match(data.toString(), /Connection:\s*keep-alive/i) - assert.match(data.toString(), /200 OK/i) + client1.once('data', data => { + assert.match(data.toString(), /Connection:\s*keep-alive/i) + assert.match(data.toString(), /200 OK/i) - // Test that fastify closes the TCP connection - client1.once('close', () => { - assert.pass() - }) + // Test that fastify closes the TCP connection + client1.once('close', () => { + assert.pass() }) }) - } - ) - }) + }) + } + ) +}) - test('Current idle connection close after server close "connection: close" header - return503OnClosing: false', assert => { - assert.plan(3) - launch('./tests/modules/immediate-close-module', {}).then( - (fastifyInstance) => { - const { port } = fastifyInstance.server.address() +test('Current idle connection close after server close "connection: close" header - return503OnClosing: false', assert => { + assert.plan(3) + launch('./tests/modules/immediate-close-module', {}).then( + (fastifyInstance) => { + const { port } = fastifyInstance.server.address() - const client = net.createConnection({ port, host: '127.0.0.1' }, () => { - client.write('GET /close HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n') + const client = net.createConnection({ port, host: '127.0.0.1' }, () => { + client.write('GET /close HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n') - client.once('data', data => { - assert.match(data.toString(), /Connection:\s*keep-alive/i) - assert.match(data.toString(), /200 OK/i) + client.once('data', data => { + assert.match(data.toString(), /Connection:\s*keep-alive/i) + assert.match(data.toString(), /200 OK/i) - // Test that fastify force close of the TCP connection - client.once('close', () => { - assert.pass() - }) + // Test that fastify force close of the TCP connection + client.once('close', () => { + assert.pass() }) }) - } - ) - }) -} + }) + } + ) +}) test('Current opened connection should not accept new incoming connections', assert => { launch('./tests/modules/immediate-close-module', {}).then( @@ -754,29 +503,6 @@ test('Current opened connection should not accept new incoming connections', ass ) }) -test('should wait at least 1 sec before closing the process', assert => { - const WAIT_BEFORE_SERVER_CLOSE_SEC = 1 - const child = spawn( - './bin/cli.js', - ['tests/modules/correct-module.js'], - { env: { ...process.env, WAIT_BEFORE_SERVER_CLOSE_SEC } } - ) - - let closedDate = null - child.on('close', () => { - closedDate = new Date() - }) - - child.stdout.on('data', () => { - const killedDate = new Date() - child.kill('SIGTERM') - setTimeout(() => { - assert.ok(closedDate.getTime() - killedDate.getTime() > WAIT_BEFORE_SERVER_CLOSE_SEC * 1000) - assert.end() - }, (WAIT_BEFORE_SERVER_CLOSE_SEC * 1000) + 500) - }) -}) - test('path with and without trailing slash', async assert => { const options = { logLevel: 'silent', diff --git a/tests/modules/custom-metrics-with-options.js b/tests/modules/custom-metrics-with-options.js index 7cb1928..8007d32 100644 --- a/tests/modules/custom-metrics-with-options.js +++ b/tests/modules/custom-metrics-with-options.js @@ -18,8 +18,17 @@ 'use strict' -module.exports = async function plugin(fastify) { - fastify.get('/', { schema: { querystring: { label: { type: 'string' } } } }, function returnConfig(request, reply) { +const schema = { + querystring: { + type: 'object', + properties: { + label: { type: 'string' }, + }, + }, +} + +module.exports = async function plugin(/** @type {import('fastify').FastifyInstance} fastify */ fastify) { + fastify.get('/', { schema }, function returnConfig(request, reply) { const { label } = request.query if (label) { this.customMetrics.collectionInvocation.inc({ label }) diff --git a/tests/modules/custom-metrics.js b/tests/modules/custom-metrics.js index 098f86a..8a3dc87 100644 --- a/tests/modules/custom-metrics.js +++ b/tests/modules/custom-metrics.js @@ -1,3 +1,4 @@ +/* eslint-disable valid-jsdoc */ /* * Copyright 2019 Mia srl * @@ -18,9 +19,19 @@ 'use strict' -module.exports = async function plugin(fastify) { - fastify.get('/', { schema: { querystring: { label: { type: 'string' } } } }, function returnConfig(request, reply) { +const schema = { + querystring: { + type: 'object', + properties: { + label: { type: 'string' }, + }, + }, +} + +module.exports = async function plugin(/** @type {import('fastify').FastifyInstance} fastify */ fastify) { + fastify.get('/', { schema }, function returnConfig(request, reply) { const { label } = request.query + if (label) { this.customMetrics.collectionInvocation.inc({ label }) } else { diff --git a/tests/modules/module-with-transform-schema.js b/tests/modules/module-with-transform-schema.js index 33deb74..9086b09 100644 --- a/tests/modules/module-with-transform-schema.js +++ b/tests/modules/module-with-transform-schema.js @@ -1,20 +1,21 @@ 'use strict' -module.exports = async function plugin(fastify) { - fastify.get('/', { - schema: { - querystring: { - type: 'object', - properties: { - label: { type: 'string' }, - }, - }, +const schema = { + querystring: { + type: 'object', + properties: { + label: { type: 'string' }, }, - }, function returnConfig(request, reply) { + }, +} + +module.exports = async function plugin(/** @type {import('fastify').FastifyInstance} fastify */ fastify) { + fastify.get('/', { schema }, function returnConfig(request, reply) { reply.send({ }) }) } +// eslint-disable-next-line no-shadow module.exports.transformSchemaForSwagger = ({ schema, url } = {}) => { if (!schema) { return { diff --git a/tests/options-extractors.test.js b/tests/options-extractors.test.js index 2964215..0ccf527 100644 --- a/tests/options-extractors.test.js +++ b/tests/options-extractors.test.js @@ -29,8 +29,10 @@ test('Test Fastify server options generator', assert => { const fastifyOptions = exportFastifyOptions(moduleOptions) assert.strictSame({ return503OnClosing: false, - ignoreTrailingSlash: false, - caseSensitive: true, + routerOptions: { + ignoreTrailingSlash: false, + caseSensitive: true, + }, requestIdHeader: 'x-request-id', pluginTimeout: 30000, bodyLimit: moduleOptions.bodyLimit, @@ -52,8 +54,10 @@ test('Test Fastify server options overwriting', assert => { const fastifyOptions = exportFastifyOptions(moduleOptions) assert.strictSame({ return503OnClosing: true, - ignoreTrailingSlash: true, - caseSensitive: true, + routerOptions: { + ignoreTrailingSlash: true, + caseSensitive: true, + }, requestIdHeader: 'x-request-id', pluginTimeout: 42, bodyLimit: Number.MAX_SAFE_INTEGER,