diff --git a/test/lib/custom-assertions.js b/test/lib/custom-assertions.js index 3fa0ddbddf..6b4a1eba1f 100644 --- a/test/lib/custom-assertions.js +++ b/test/lib/custom-assertions.js @@ -33,10 +33,18 @@ function assertCLMAttrs({ segments, enabled: clmEnabled, skipFull = false }) { const attrs = segment.segment.getAttributes() if (clmEnabled) { assert.equal(attrs['code.function'], segment.name, 'should have appropriate code.function') - assert.ok( - attrs['code.filepath'].endsWith(segment.filepath), - 'should have appropriate code.filepath' - ) + if (segment.filepath instanceof RegExp) { + assert.match( + attrs['code.filepath'], + segment.filepath, + 'should have appropriate code.filepath' + ) + } else { + assert.ok( + attrs['code.filepath'].endsWith(segment.filepath), + 'should have appropriate code.filepath' + ) + } if (!skipFull) { assert.equal(typeof attrs['code.lineno'], 'number', 'lineno should be a number') diff --git a/test/versioned/fastify/add-hook.tap.js b/test/versioned/fastify/add-hook.tap.js deleted file mode 100644 index 2b489904ff..0000000000 --- a/test/versioned/fastify/add-hook.tap.js +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright 2021 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const tap = require('tap') -const helper = require('../../lib/agent_helper') -require('../../lib/metrics_helper') -const common = require('./common') - -// all of these events fire before the route handler -// See: https://www.fastify.io/docs/latest/Lifecycle/ -// for more info on sequence -const REQUEST_HOOKS = ['onRequest', 'preParsing', 'preValidation', 'preHandler'] - -// these events fire after the route -// handler. they are in separate arrays -// for segment relationship assertions later -const AFTER_HANDLER_HOOKS = ['preSerialization', 'onSend'] - -// the onResponse hook fires after a response -// is received by client which is out of context -// of the transaction -const AFTER_TX_HOOKS = ['onResponse'] - -const ALL_HOOKS = [...REQUEST_HOOKS, ...AFTER_HANDLER_HOOKS, ...AFTER_TX_HOOKS] - -/** - * Helper to return the list of expected segments - * - * @param {Array} hooks lifecyle hook names to build segment names from - * @returns {Array} formatted list of expected segments - */ -function getExpectedSegments(hooks) { - return hooks.map((hookName) => { - return `Nodejs/Middleware/Fastify/${hookName}/testHook` - }) -} - -tap.test('fastify hook instrumentation', (t) => { - t.autoend() - t.beforeEach(() => { - const agent = helper.instrumentMockedAgent() - const fastify = require('fastify')() - common.setupRoutes(fastify) - t.context.agent = agent - t.context.fastify = fastify - }) - - t.afterEach(() => { - const { fastify, agent } = t.context - helper.unloadAgent(agent) - fastify.close() - }) - - t.test('non-error hooks', async function nonErrorHookTest(t) { - const { fastify, agent } = t.context - - // setup hooks - const ok = ALL_HOOKS.reduce((all, hookName) => { - all[hookName] = false - return all - }, {}) - - ALL_HOOKS.forEach((hookName) => { - fastify.addHook(hookName, function testHook(...args) { - // lifecycle signatures vary between the events - // the last arg is always the next function though - const next = args[args.length - 1] - ok[hookName] = true - next() - }) - }) - - agent.on('transactionFinished', (transaction) => { - t.equal( - 'WebFrameworkUri/Fastify/GET//add-hook', - transaction.getName(), - `transaction name matched` - ) - // all the hooks are siblings of the route handler - // except the AFTER_HANDLER_HOOKS which are children of the route handler - let expectedSegments - if (helper.isSecurityAgentEnabled(agent)) { - expectedSegments = [ - 'WebTransaction/WebFrameworkUri/Fastify/GET//add-hook', - [ - 'Nodejs/Middleware/Fastify/onRequest/', - [ - ...getExpectedSegments(REQUEST_HOOKS), - 'Nodejs/Middleware/Fastify/routeHandler//add-hook', - getExpectedSegments(AFTER_HANDLER_HOOKS) - ] - ] - ] - } else { - expectedSegments = [ - 'WebTransaction/WebFrameworkUri/Fastify/GET//add-hook', - [ - ...getExpectedSegments(REQUEST_HOOKS), - 'Nodejs/Middleware/Fastify/routeHandler//add-hook', - getExpectedSegments(AFTER_HANDLER_HOOKS) - ] - ] - } - t.assertSegments(transaction.trace.root, expectedSegments) - }) - - await fastify.listen({ port: 0 }) - const address = fastify.server.address() - const result = await common.makeRequest(address, '/add-hook') - t.same(result, { hello: 'world' }) - - // verify every hook was called after response - for (const [hookName, isOk] of Object.entries(ok)) { - t.equal(isOk, true, `${hookName} captured`) - } - t.end() - }) - - t.test('error hook', async function errorHookTest(t) { - const { fastify, agent } = t.context - - const hookName = 'onError' - let ok = false - - fastify.addHook(hookName, function testHook(req, reply, err, next) { - t.equal(err.message, 'test onError hook', 'error message correct') - ok = true - next() - }) - - agent.on('transactionFinished', (transaction) => { - t.equal( - 'WebFrameworkUri/Fastify/GET//error', - transaction.getName(), - `transaction name matched` - ) - // all the hooks are siblings of the route handler - let expectedSegments - if (helper.isSecurityAgentEnabled(agent)) { - expectedSegments = [ - 'WebTransaction/WebFrameworkUri/Fastify/GET//error', - [ - 'Nodejs/Middleware/Fastify/onRequest/', - [ - 'Nodejs/Middleware/Fastify/errorRoute//error', - [`Nodejs/Middleware/Fastify/${hookName}/testHook`] - ] - ] - ] - } else { - expectedSegments = [ - 'WebTransaction/WebFrameworkUri/Fastify/GET//error', - [ - 'Nodejs/Middleware/Fastify/errorRoute//error', - [`Nodejs/Middleware/Fastify/${hookName}/testHook`] - ] - ] - } - - t.assertSegments(transaction.trace.root, expectedSegments) - }) - - await fastify.listen({ port: 0 }) - const address = fastify.server.address() - const result = await common.makeRequest(address, '/error') - t.ok(ok) - t.same(result, { - statusCode: 500, - error: 'Internal Server Error', - message: 'test onError hook' - }) - t.end() - }) -}) diff --git a/test/versioned/fastify/add-hook.test.js b/test/versioned/fastify/add-hook.test.js new file mode 100644 index 0000000000..b836dfcde3 --- /dev/null +++ b/test/versioned/fastify/add-hook.test.js @@ -0,0 +1,187 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const test = require('node:test') +const assert = require('node:assert') + +const { removeModules } = require('../../lib/cache-buster') +const { assertSegments, match } = require('../../lib/custom-assertions') +const helper = require('../../lib/agent_helper') +const common = require('./common') + +// all of these events fire before the route handler +// See: https://www.fastify.io/docs/latest/Lifecycle/ +// for more info on sequence +const REQUEST_HOOKS = ['onRequest', 'preParsing', 'preValidation', 'preHandler'] + +// these events fire after the route +// handler. they are in separate arrays +// for segment relationship assertions later +const AFTER_HANDLER_HOOKS = ['preSerialization', 'onSend'] + +// the onResponse hook fires after a response +// is received by client which is out of context +// of the transaction +const AFTER_TX_HOOKS = ['onResponse'] + +const ALL_HOOKS = [...REQUEST_HOOKS, ...AFTER_HANDLER_HOOKS, ...AFTER_TX_HOOKS] + +/** + * Helper to return the list of expected segments + * + * @param {Array} hooks lifecyle hook names to build segment names from + * @returns {Array} formatted list of expected segments + */ +function getExpectedSegments(hooks) { + return hooks.map((hookName) => { + return `Nodejs/Middleware/Fastify/${hookName}/testHook` + }) +} + +test.beforeEach((ctx) => { + ctx.nr = {} + ctx.nr.agent = helper.instrumentMockedAgent() + + const fastify = require('fastify')() + common.setupRoutes(fastify) + ctx.nr.fastify = fastify +}) + +test.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) + ctx.nr.fastify.close() + removeModules(['fastify']) +}) + +test('non-error hooks', async (t) => { + const { fastify, agent } = t.nr + + // setup hooks + const ok = ALL_HOOKS.reduce((all, hookName) => { + all[hookName] = false + return all + }, {}) + + ALL_HOOKS.forEach((hookName) => { + fastify.addHook(hookName, function testHook(...args) { + // lifecycle signatures vary between the events + // the last arg is always the next function though + const next = args[args.length - 1] + ok[hookName] = true + next() + }) + }) + + let txPassed = false + agent.on('transactionFinished', (transaction) => { + assert.equal( + 'WebFrameworkUri/Fastify/GET//add-hook', + transaction.getName(), + `transaction name matched` + ) + // all the hooks are siblings of the route handler + // except the AFTER_HANDLER_HOOKS which are children of the route handler + let expectedSegments + if (helper.isSecurityAgentEnabled(agent)) { + expectedSegments = [ + 'WebTransaction/WebFrameworkUri/Fastify/GET//add-hook', + [ + 'Nodejs/Middleware/Fastify/onRequest/', + [ + ...getExpectedSegments(REQUEST_HOOKS), + 'Nodejs/Middleware/Fastify/routeHandler//add-hook', + getExpectedSegments(AFTER_HANDLER_HOOKS) + ] + ] + ] + } else { + expectedSegments = [ + 'WebTransaction/WebFrameworkUri/Fastify/GET//add-hook', + [ + ...getExpectedSegments(REQUEST_HOOKS), + 'Nodejs/Middleware/Fastify/routeHandler//add-hook', + getExpectedSegments(AFTER_HANDLER_HOOKS) + ] + ] + } + assertSegments(transaction.trace.root, expectedSegments) + + txPassed = true + }) + + await fastify.listen({ port: 0 }) + const address = fastify.server.address() + const result = await common.makeRequest(address, '/add-hook') + match(result, { hello: 'world' }) + + // verify every hook was called after response + for (const [hookName, isOk] of Object.entries(ok)) { + assert.equal(isOk, true, `${hookName} captured`) + } + + assert.equal(txPassed, true, 'transactionFinished assertions passed') +}) + +test('error hook', async function errorHookTest(t) { + const { fastify, agent } = t.nr + + const hookName = 'onError' + let ok = false + + fastify.addHook(hookName, function testHook(req, reply, err, next) { + assert.equal(err.message, 'test onError hook', 'error message correct') + ok = true + next() + }) + + let txPassed = false + agent.on('transactionFinished', (transaction) => { + assert.equal( + 'WebFrameworkUri/Fastify/GET//error', + transaction.getName(), + `transaction name matched` + ) + // all the hooks are siblings of the route handler + let expectedSegments + if (helper.isSecurityAgentEnabled(agent)) { + expectedSegments = [ + 'WebTransaction/WebFrameworkUri/Fastify/GET//error', + [ + 'Nodejs/Middleware/Fastify/onRequest/', + [ + 'Nodejs/Middleware/Fastify/errorRoute//error', + [`Nodejs/Middleware/Fastify/${hookName}/testHook`] + ] + ] + ] + } else { + expectedSegments = [ + 'WebTransaction/WebFrameworkUri/Fastify/GET//error', + [ + 'Nodejs/Middleware/Fastify/errorRoute//error', + [`Nodejs/Middleware/Fastify/${hookName}/testHook`] + ] + ] + } + + assertSegments(transaction.trace.root, expectedSegments) + + txPassed = true + }) + + await fastify.listen({ port: 0 }) + const address = fastify.server.address() + const result = await common.makeRequest(address, '/error') + assert.ok(ok) + match(result, { + statusCode: 500, + error: 'Internal Server Error', + message: 'test onError hook' + }) + + assert.equal(txPassed, true, 'transactionFinished assertions passed') +}) diff --git a/test/versioned/fastify/code-level-metrics-hooks.tap.js b/test/versioned/fastify/code-level-metrics-hooks.tap.js deleted file mode 100644 index c86d9b451d..0000000000 --- a/test/versioned/fastify/code-level-metrics-hooks.tap.js +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2022 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const tap = require('tap') -const helper = require('../../lib/agent_helper') -const common = require('./common') - -function setupFastifyServer(fastify) { - common.setupRoutes(fastify) -} - -function setup(test, config) { - const agent = helper.instrumentMockedAgent(config) - const fastify = require('fastify')() - - setupFastifyServer(fastify) - - test.context.agent = agent - test.context.fastify = fastify - - test.teardown(() => { - helper.unloadAgent(agent) - fastify.close() - }) -} - -tap.test('Fastify CLM Hook Based', (test) => { - test.autoend() - ;[true, false].forEach((isCLMEnabled) => { - test.test(isCLMEnabled ? 'should add attributes' : 'should not add attributes', async (t) => { - setup(t, { code_level_metrics: { enabled: isCLMEnabled } }) - const { agent, fastify } = t.context - - fastify.addHook('onRequest', function testOnRequest(...args) { - const next = args.pop() - next() - }) - - fastify.addHook('onSend', function testOnSend(...args) { - const next = args.pop() - next() - }) - - agent.on('transactionFinished', (transaction) => { - const baseSegment = transaction.trace.root.children - const [onRequestSegment, handlerSegment] = helper.isSecurityAgentEnabled(agent) - ? baseSegment[0].children[0].children - : baseSegment[0].children - const onSendSegment = handlerSegment.children[0] - t.clmAttrs({ - segments: [ - { - segment: onRequestSegment, - name: 'testOnRequest', - filepath: 'test/versioned/fastify/code-level-metrics-hooks.tap.js' - }, - { - segment: onSendSegment, - name: 'testOnSend', - filepath: 'test/versioned/fastify/code-level-metrics-hooks.tap.js' - }, - { - segment: handlerSegment, - name: 'routeHandler', - filepath: 'test/versioned/fastify/common.js' - } - ], - enabled: isCLMEnabled - }) - }) - - await fastify.listen({ port: 0 }) - const address = fastify.server.address() - const result = await common.makeRequest(address, '/add-hook') - - t.same(result, { hello: 'world' }) - }) - }) -}) diff --git a/test/versioned/fastify/code-level-metrics-hooks.test.js b/test/versioned/fastify/code-level-metrics-hooks.test.js new file mode 100644 index 0000000000..2ed4825e30 --- /dev/null +++ b/test/versioned/fastify/code-level-metrics-hooks.test.js @@ -0,0 +1,96 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const test = require('node:test') +const assert = require('node:assert') + +const { removeModules } = require('../../lib/cache-buster') +const { assertCLMAttrs, match } = require('../../lib/custom-assertions') +const helper = require('../../lib/agent_helper') +const common = require('./common') + +test.beforeEach((ctx) => { + ctx.nr = { agent: null, fastify: null } +}) + +test.afterEach((ctx) => { + if (ctx.nr.agent) { + helper.unloadAgent(ctx.nr.agent) + } + + if (ctx.nr.fastify) { + ctx.nr.fastify.close() + } + + removeModules(['fastify']) +}) + +async function performTest(t) { + const { agent, fastify } = t.nr + fastify.addHook('onRequest', function testOnRequest(...args) { + const next = args.pop() + next() + }) + + fastify.addHook('onSend', function testOnSend(...args) { + const next = args.pop() + next() + }) + + let txPassed = false + agent.on('transactionFinished', (transaction) => { + const baseSegment = transaction.trace.root.children + const [onRequestSegment, handlerSegment] = helper.isSecurityAgentEnabled(agent) + ? baseSegment[0].children[0].children + : baseSegment[0].children + const onSendSegment = handlerSegment.children[0] + assertCLMAttrs({ + segments: [ + { + segment: onRequestSegment, + name: 'testOnRequest', + filepath: 'test/versioned/fastify/code-level-metrics-hooks.test.js' + }, + { + segment: onSendSegment, + name: 'testOnSend', + filepath: 'test/versioned/fastify/code-level-metrics-hooks.test.js' + }, + { + segment: handlerSegment, + name: 'routeHandler', + filepath: 'test/versioned/fastify/common.js' + } + ], + enabled: agent.config.code_level_metrics.enabled + }) + + txPassed = true + }) + + await fastify.listen({ port: 0 }) + const address = fastify.server.address() + const result = await common.makeRequest(address, '/add-hook') + + match(result, { hello: 'world' }) + + assert.equal(txPassed, true, 'transactionFinished assertions passed') +} + +test('should add attributes', async (t) => { + t.nr.agent = helper.instrumentMockedAgent({ code_level_metrics: { enabled: true } }) + t.nr.fastify = require('fastify')() + common.setupRoutes(t.nr.fastify) + await performTest(t) +}) + +test('should not add attributes', async (t) => { + t.nr.agent = helper.instrumentMockedAgent({ code_level_metrics: { enabled: false } }) + t.nr.fastify = require('fastify')() + common.setupRoutes(t.nr.fastify) + await performTest(t) +}) diff --git a/test/versioned/fastify/code-level-metrics-middleware.tap.js b/test/versioned/fastify/code-level-metrics-middleware.tap.js deleted file mode 100644 index 0497a975b9..0000000000 --- a/test/versioned/fastify/code-level-metrics-middleware.tap.js +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright 2022 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const tap = require('tap') -const helper = require('../../lib/agent_helper') -const common = require('./common') -const semver = require('semver') -const { version: pkgVersion } = require('fastify/package') - -async function setupFastifyServer(fastify, calls) { - common.setupRoutes(fastify) - await fastify.register(require('middie')) - common.registerMiddlewares({ fastify, calls }) -} - -function setupFastifyServerV2(fastify, calls) { - common.setupRoutes(fastify) - common.registerMiddlewares({ fastify, calls }) -} - -async function setup(test, config) { - const agent = helper.instrumentMockedAgent(config) - const fastify = require('fastify')() - const calls = { test: 0, middleware: 0 } - - // TODO: once we drop v2 support, update to use `setupFastifyServer` exclusively - const setupServerFn = semver.satisfies(pkgVersion, '>=3') - ? setupFastifyServer - : setupFastifyServerV2 - - await setupServerFn(fastify, calls) - - test.context.agent = agent - test.context.fastify = fastify - test.context.calls = calls - - test.teardown(() => { - helper.unloadAgent(agent) - fastify.close() - }) -} - -function assertSegments(test, baseSegment, isCLMEnabled) { - const { agent } = test.context - const { children } = helper.isSecurityAgentEnabled(agent) ? baseSegment.children[0] : baseSegment - // TODO: once we drop v2 support, this function can be removed and assert inline in test below - if (semver.satisfies(pkgVersion, '>=3')) { - const [middieSegment, handlerSegment] = children - test.clmAttrs({ - segments: [ - { - segment: middieSegment, - name: 'runMiddie', - filepath: 'test/versioned/fastify/node_modules/middie/index.js' - }, - { - segment: handlerSegment, - name: '(anonymous)', - filepath: 'test/versioned/fastify/common.js' - } - ], - enabled: isCLMEnabled - }) - } else { - const [middieSegment, mwSegment, handlerSegment] = children - test.clmAttrs({ - segments: [ - { - segment: middieSegment, - name: 'testMiddleware', - filepath: 'test/versioned/fastify/common.js' - }, - { - segment: mwSegment, - name: 'pathMountedMiddleware', - filepath: 'test/versioned/fastify/common.js' - }, - { - segment: handlerSegment, - name: '(anonymous)', - filepath: 'test/versioned/fastify/common.js' - } - ], - enabled: isCLMEnabled - }) - } -} - -tap.test('Fastify CLM Middleware Based', (test) => { - test.autoend() - ;[true, false].forEach((isCLMEnabled) => { - test.test(isCLMEnabled ? 'should add attributes' : 'should not add attributes', async (t) => { - await setup(t, { code_level_metrics: { enabled: isCLMEnabled } }) - const { agent, fastify, calls } = t.context - const uri = common.routesToTest[0] - - agent.on('transactionFinished', (transaction) => { - calls.test++ - assertSegments(t, transaction.trace.root.children[0], isCLMEnabled) - }) - - await fastify.listen({ port: 0 }) - const address = fastify.server.address() - const result = await common.makeRequest(address, uri) - - t.equal(result.called, uri, `${uri} url did not error`) - t.ok(calls.test > 0) - t.equal(calls.test, calls.middleware, 'should be the same value') - }) - }) -}) diff --git a/test/versioned/fastify/code-level-metrics-middleware.test.js b/test/versioned/fastify/code-level-metrics-middleware.test.js new file mode 100644 index 0000000000..0449d88310 --- /dev/null +++ b/test/versioned/fastify/code-level-metrics-middleware.test.js @@ -0,0 +1,128 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const test = require('node:test') +const assert = require('node:assert') +const semver = require('semver') + +const { version: pkgVersion } = require('fastify/package') + +const { removeModules } = require('../../lib/cache-buster') +const { assertCLMAttrs } = require('../../lib/custom-assertions') +const helper = require('../../lib/agent_helper') +const common = require('./common') + +test.beforeEach((ctx) => { + ctx.nr = { agent: null, fastify: null, calls: { test: 0, middleware: 0 } } +}) + +test.afterEach((ctx) => { + if (ctx.nr.agent) { + helper.unloadAgent(ctx.nr.agent) + } + + if (ctx.nr.fastify) { + ctx.nr.fastify.close() + } + + removeModules(['fastify', '@fastify/middie', 'middie']) +}) + +async function setup(t, config) { + t.nr.agent = helper.instrumentMockedAgent(config) + t.nr.fastify = require('fastify')() + + const { fastify, calls } = t.nr + if (semver.satisfies(pkgVersion, '>=3') === true) { + common.setupRoutes(fastify) + + if (semver.major(pkgVersion) < 4) { + await fastify.register(require('middie')) + } else { + await fastify.register(require('@fastify/middie')) + } + common.registerMiddlewares({ fastify, calls }) + } else { + // TODO: once we drop v2 support remove this case + common.setupRoutes(fastify) + common.registerMiddlewares({ fastify, calls }) + } +} + +function assertSegments(testContext, baseSegment, isCLMEnabled) { + const { agent } = testContext.nr + const { children } = helper.isSecurityAgentEnabled(agent) ? baseSegment.children[0] : baseSegment + // TODO: once we drop v2 support, this function can be removed and assert inline in test below + if (semver.satisfies(pkgVersion, '>=3')) { + const [middieSegment, handlerSegment] = children + assertCLMAttrs({ + segments: [ + { + segment: middieSegment, + name: 'runMiddie', + filepath: /test\/versioned\/fastify\/node_modules\/(@fastify)?\/middie\/index.js/ + }, + { + segment: handlerSegment, + name: '(anonymous)', + filepath: 'test/versioned/fastify/common.js' + } + ], + enabled: isCLMEnabled + }) + } else { + const [middieSegment, mwSegment, handlerSegment] = children + assertCLMAttrs({ + segments: [ + { + segment: middieSegment, + name: 'testMiddleware', + filepath: 'test/versioned/fastify/common.js' + }, + { + segment: mwSegment, + name: 'pathMountedMiddleware', + filepath: 'test/versioned/fastify/common.js' + }, + { + segment: handlerSegment, + name: '(anonymous)', + filepath: 'test/versioned/fastify/common.js' + } + ], + enabled: isCLMEnabled + }) + } +} + +async function performTest(t) { + const { agent, fastify, calls } = t.nr + const uri = common.routesToTest[0] + + agent.on('transactionFinished', (transaction) => { + calls.test++ + assertSegments(t, transaction.trace.root.children[0], agent.config.code_level_metrics.enabled) + }) + + await fastify.listen({ port: 0 }) + const address = fastify.server.address() + const result = await common.makeRequest(address, uri) + + assert.equal(result.called, uri, `${uri} url did not error`) + assert.ok(calls.test > 0) + assert.equal(calls.test, calls.middleware, 'should be the same value') +} + +test('should add attributes', async (t) => { + await setup(t, { code_level_metrics: { enabled: true } }) + await performTest(t) +}) + +test('should not add attributes', async (t) => { + await setup(t, { code_level_metrics: { enabled: false } }) + await performTest(t) +}) diff --git a/test/versioned/fastify/common.js b/test/versioned/fastify/common.js index 864954915f..438f3844c3 100644 --- a/test/versioned/fastify/common.js +++ b/test/versioned/fastify/common.js @@ -84,7 +84,7 @@ common.setupRoutes = (fastify) => { /** * Defines both a global middleware and middleware mounted at a specific - * path. This tests the `middie`, and/or `fastify-express` plugin middlewawre + * path. This tests the `middie` and/or `fastify-express` plugin middleware * instrumentation */ common.registerMiddlewares = ({ fastify, calls }) => { diff --git a/test/versioned/fastify/errors.tap.js b/test/versioned/fastify/errors.test.js similarity index 65% rename from test/versioned/fastify/errors.tap.js rename to test/versioned/fastify/errors.test.js index 587e0550aa..71d50c05f8 100644 --- a/test/versioned/fastify/errors.tap.js +++ b/test/versioned/fastify/errors.test.js @@ -1,27 +1,33 @@ /* - * Copyright 2021 New Relic Corporation. All rights reserved. + * Copyright 2024 New Relic Corporation. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ 'use strict' -const tap = require('tap') -const helper = require('../../lib/agent_helper') +const test = require('node:test') +const assert = require('node:assert') const semver = require('semver') + +const helper = require('../../lib/agent_helper') const { makeRequest } = require('./common') -tap.test('Test Errors', async (test) => { +test('Test Errors', async (t) => { const agent = helper.instrumentMockedAgent() const fastify = require('fastify')() const { version: pkgVersion } = require('fastify/package') - test.teardown(() => { + t.after(() => { helper.unloadAgent(agent) fastify.close() }) if (semver.satisfies(pkgVersion, '>=3')) { - await fastify.register(require('middie')) + if (semver.major(pkgVersion) < 4) { + await fastify.register(require('middie')) + } else { + await fastify.register(require('@fastify/middie')) + } } fastify.use((req, res, next) => { @@ -34,6 +40,5 @@ tap.test('Test Errors', async (test) => { await fastify.listen({ port: 0 }) const address = fastify.server.address() const res = await makeRequest(address, '/404-via-reply') - test.equal(res.statusCode, 404) - test.end() + assert.equal(res.statusCode, 404) }) diff --git a/test/versioned/fastify/fastify-2-naming.tap.js b/test/versioned/fastify/fastify-2-naming.tap.js deleted file mode 100644 index b0f685edca..0000000000 --- a/test/versioned/fastify/fastify-2-naming.tap.js +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2021 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const tap = require('tap') -const helper = require('../../lib/agent_helper') -const common = require('./common') -const createTests = require('./naming-common') - -/** - * Helper to return the list of expected segments - * - * @param {Array} uri - * @returns {Array} formatted list of expected segments - */ -function getExpectedSegments(uri) { - return [ - 'Nodejs/Middleware/Fastify/onRequest/testMiddleware', - `Nodejs/Middleware/Fastify/onRequest/pathMountedMiddleware/${uri}`, - `Nodejs/Middleware/Fastify//${uri}` - ] -} - -tap.test('Test Transaction Naming', (test) => { - test.autoend() - - test.beforeEach(() => { - const agent = helper.instrumentMockedAgent() - const fastify = require('fastify')() - const calls = { test: 0, middleware: 0 } - test.context.agent = agent - test.context.fastify = fastify - test.context.calls = calls - common.setupRoutes(fastify) - common.registerMiddlewares({ fastify, calls }) - }) - - test.afterEach(() => { - const { fastify, agent } = test.context - helper.unloadAgent(agent) - fastify.close() - }) - - createTests(test, getExpectedSegments) -}) diff --git a/test/versioned/fastify/fastify-2-naming.test.js b/test/versioned/fastify/fastify-2-naming.test.js new file mode 100644 index 0000000000..a20046c50f --- /dev/null +++ b/test/versioned/fastify/fastify-2-naming.test.js @@ -0,0 +1,50 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const test = require('node:test') + +const { removeModules } = require('../../lib/cache-buster') +const helper = require('../../lib/agent_helper') +const common = require('./common') +const runTests = require('./naming-common') + +/** + * Helper to return the list of expected segments + * + * @param {Array} uri + * @returns {Array} formatted list of expected segments + */ +function getExpectedSegments(uri) { + return [ + 'Nodejs/Middleware/Fastify/onRequest/testMiddleware', + `Nodejs/Middleware/Fastify/onRequest/pathMountedMiddleware/${uri}`, + `Nodejs/Middleware/Fastify//${uri}` + ] +} + +test.beforeEach((ctx) => { + ctx.nr = {} + ctx.nr.agent = helper.instrumentMockedAgent() + + const calls = { test: 0, middleware: 0 } + const fastify = require('fastify')() + common.setupRoutes(fastify) + common.registerMiddlewares({ fastify, calls }) + ctx.nr.calls = calls + ctx.nr.fastify = fastify +}) + +test.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) + ctx.nr.fastify.close() + + removeModules(['fastify']) +}) + +test('fastify@2 transaction naming', async (t) => { + await runTests(t, getExpectedSegments) +}) diff --git a/test/versioned/fastify/fastify-3-naming.tap.js b/test/versioned/fastify/fastify-3-naming.tap.js deleted file mode 100644 index ac15ed2331..0000000000 --- a/test/versioned/fastify/fastify-3-naming.tap.js +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright 2021 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const tap = require('tap') -const helper = require('../../lib/agent_helper') -const common = require('./common') -const createTests = require('./naming-common') - -async function setupFastifyServer(fastify, calls) { - common.setupRoutes(fastify) - await fastify.register(require('middie')) - common.registerMiddlewares({ fastify, calls }) -} - -/** - * Helper to return the list of expected segments - * - * @param {Array} uri - * @returns {Array} formatted list of expected segments - */ -function getExpectedSegments(uri) { - return [ - 'Nodejs/Middleware/Fastify/onRequest/runMiddie', - [ - 'Nodejs/Middleware/Fastify/onRequest/testMiddleware', - `Nodejs/Middleware/Fastify/onRequest/pathMountedMiddleware/${uri}` - ], - `Nodejs/Middleware/Fastify//${uri}` - ] -} - -tap.test('Test Transaction Naming - Standard Export', (test) => { - test.autoend() - - test.beforeEach(async (t) => { - const agent = helper.instrumentMockedAgent() - const fastify = require('fastify')() - const calls = { test: 0, middleware: 0 } - await setupFastifyServer(fastify, calls) - - t.context.agent = agent - t.context.fastify = fastify - t.context.calls = calls - }) - - test.afterEach((t) => { - const { agent, fastify } = t.context - - helper.unloadAgent(agent) - fastify.close() - }) - - createTests(test, getExpectedSegments) -}) - -tap.test('Test Transaction Naming - Fastify Property', (test) => { - test.autoend() - - test.beforeEach(async (t) => { - const agent = helper.instrumentMockedAgent({ - feature_flag: { - fastify_instrumentation: true - } - }) - const fastify = require('fastify').fastify() - const calls = { test: 0, middleware: 0 } - await setupFastifyServer(fastify, calls) - - t.context.agent = agent - t.context.fastify = fastify - t.context.calls = calls - }) - - test.afterEach((t) => { - const { agent, fastify } = t.context - - helper.unloadAgent(agent) - fastify.close() - }) - - createTests(test, getExpectedSegments) -}) - -tap.test('Test Transaction Naming - Default Property', (test) => { - test.autoend() - - test.beforeEach(async (t) => { - const agent = helper.instrumentMockedAgent({ - feature_flag: { - fastify_instrumentation: true - } - }) - const fastify = require('fastify').default() - const calls = { test: 0, middleware: 0 } - await setupFastifyServer(fastify, calls) - - t.context.agent = agent - t.context.fastify = fastify - t.context.calls = calls - }) - - test.afterEach((t) => { - const { agent, fastify } = t.context - - helper.unloadAgent(agent) - fastify.close() - }) - - createTests(test, getExpectedSegments) -}) diff --git a/test/versioned/fastify/fastify-3.tap.js b/test/versioned/fastify/fastify-3.tap.js deleted file mode 100644 index 0adf7d50f9..0000000000 --- a/test/versioned/fastify/fastify-3.tap.js +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2021 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const tap = require('tap') - -const helper = require('../../lib/agent_helper') -const symbols = require('../../../lib/symbols') - -tap.test('Fastify Instrumentation', (t) => { - t.autoend() - - let agent = null - let fastifyExport = null - - t.beforeEach(() => { - agent = helper.instrumentMockedAgent({ - feature_flag: { - fastify_instrumentation: true - } - }) - fastifyExport = require('fastify') - }) - - t.afterEach(() => { - helper.unloadAgent(agent) - }) - - /** - * Fastify v3 has '.fastify' and '.default' properties attached to the exported - * 'fastify' function. These are all the same original exported function, just - * arranged to support a variety of import styles. - */ - t.test('Should propagate fastify exports when instrumented', (t) => { - const original = fastifyExport[symbols.original] - - // Confirms the original setup matches expectations - t.equal(original.fastify, original) - t.equal(original.default, original) - - // Asserts our new export has the same behavior - t.equal(fastifyExport.fastify, fastifyExport) - t.equal(fastifyExport.default, fastifyExport) - - t.end() - }) -}) diff --git a/test/versioned/fastify/fastify-gte3-naming.test.js b/test/versioned/fastify/fastify-gte3-naming.test.js new file mode 100644 index 0000000000..9680230029 --- /dev/null +++ b/test/versioned/fastify/fastify-gte3-naming.test.js @@ -0,0 +1,117 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const test = require('node:test') +const semver = require('semver') + +const { version: pkgVersion } = require('fastify/package') + +const { removeModules } = require('../../lib/cache-buster') +const helper = require('../../lib/agent_helper') +const common = require('./common') +const runTests = require('./naming-common') + +/** + * Helper to return the list of expected segments + * + * @param {Array} uri + * @returns {Array} formatted list of expected segments + */ +function getExpectedSegments(uri) { + return [ + 'Nodejs/Middleware/Fastify/onRequest/runMiddie', + [ + 'Nodejs/Middleware/Fastify/onRequest/testMiddleware', + `Nodejs/Middleware/Fastify/onRequest/pathMountedMiddleware/${uri}` + ], + `Nodejs/Middleware/Fastify//${uri}` + ] +} + +async function setupFastifyServer(fastify, calls) { + common.setupRoutes(fastify) + + if (semver.major(pkgVersion) === 3) { + await fastify.register(require('middie')) + } else { + await fastify.register(require('@fastify/middie')) + } + + common.registerMiddlewares({ fastify, calls }) +} + +test('standard export', async (t) => { + test.beforeEach(async (ctx) => { + ctx.nr = {} + ctx.nr.agent = helper.instrumentMockedAgent() + + const calls = { test: 0, middleware: 0 } + const fastify = require('fastify')() + await setupFastifyServer(fastify, calls) + ctx.nr.calls = calls + ctx.nr.fastify = fastify + }) + + test.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) + ctx.nr.fastify.close() + + removeModules(['fastify', '@fastify/middie', 'middie']) + }) + + await t.test(async (t) => { + await runTests(t, getExpectedSegments) + }) +}) + +test('fastify property', async (t) => { + test.beforeEach(async (ctx) => { + ctx.nr = {} + ctx.nr.agent = helper.instrumentMockedAgent() + + const calls = { test: 0, middleware: 0 } + const fastify = require('fastify').fastify() + await setupFastifyServer(fastify, calls) + ctx.nr.calls = calls + ctx.nr.fastify = fastify + }) + + test.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) + ctx.nr.fastify.close() + + removeModules(['fastify', '@fastify/middie', 'middie']) + }) + + await t.test(async (t) => { + await runTests(t, getExpectedSegments) + }) +}) + +test('default property', async (t) => { + test.beforeEach(async (ctx) => { + ctx.nr = {} + ctx.nr.agent = helper.instrumentMockedAgent() + + const calls = { test: 0, middleware: 0 } + const fastify = require('fastify').default() + await setupFastifyServer(fastify, calls) + ctx.nr.calls = calls + ctx.nr.fastify = fastify + }) + + test.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) + ctx.nr.fastify.close() + + removeModules(['fastify', '@fastify/middie', 'middie']) + }) + + await t.test(async (t) => { + await runTests(t, getExpectedSegments) + }) +}) diff --git a/test/versioned/fastify/fastify-gte3.test.js b/test/versioned/fastify/fastify-gte3.test.js new file mode 100644 index 0000000000..ddaf287c4e --- /dev/null +++ b/test/versioned/fastify/fastify-gte3.test.js @@ -0,0 +1,31 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const test = require('node:test') +const assert = require('node:assert') + +const helper = require('../../lib/agent_helper') +const symbols = require('../../../lib/symbols') + +/** + * Fastify v3 has '.fastify' and '.default' properties attached to the exported + * 'fastify' function. These are all the same original exported function, just + * arranged to support a variety of import styles. + */ +test('Should propagate fastify exports when instrumented', () => { + helper.instrumentMockedAgent() + const Fastify = require('fastify') + const original = Fastify[symbols.original] + + // Confirms the original setup matches expectations + assert.equal(original.fastify, original) + assert.equal(original.default, original) + + // Asserts our new export has the same behavior + assert.equal(Fastify.fastify, Fastify) + assert.equal(Fastify.default, Fastify) +}) diff --git a/test/versioned/fastify/naming-common.js b/test/versioned/fastify/naming-common.js index 4b692c2a6c..af4522008e 100644 --- a/test/versioned/fastify/naming-common.js +++ b/test/versioned/fastify/naming-common.js @@ -4,18 +4,25 @@ */ 'use strict' + +const assert = require('node:assert') + const { routesToTest, makeRequest } = require('./common') -require('../../lib/metrics_helper') +const { assertSegments, match } = require('../../lib/custom-assertions') const helper = require('../../lib/agent_helper') -module.exports = function createTests(t, getExpectedSegments) { - routesToTest.forEach((uri) => { - t.test(`testing naming for ${uri} `, async (t) => { - const { agent, fastify, calls } = t.context +module.exports = async function runTests(t, getExpectedSegments) { + // Since we have spawned these sub-tests from another sub-test we must + // clear out the agent before they are evaluated. + helper.unloadAgent(t.nr.agent) + + for (const uri of routesToTest) { + await t.test(`testing naming for ${uri} `, async (t) => { + const { agent, fastify, calls } = t.nr agent.on('transactionFinished', (transaction) => { calls.test++ - t.equal( + assert.equal( `WebFrameworkUri/Fastify/GET/${uri}`, transaction.getName(), `transaction name matched for ${uri}` @@ -36,33 +43,37 @@ module.exports = function createTests(t, getExpectedSegments) { ] } - t.assertSegments(transaction.trace.root, expectedSegments) + assertSegments(transaction.trace.root, expectedSegments) }) await fastify.listen({ port: 0 }) const address = fastify.server.address() const result = await makeRequest(address, uri) - t.equal(result.called, uri, `${uri} url did not error`) - t.ok(calls.test > 0) - t.equal(calls.test, calls.middleware, 'should be the same value') - t.end() + assert.equal(result.called, uri, `${uri} url did not error`) + assert.ok(calls.test > 0) + assert.equal(calls.test, calls.middleware, 'should be the same value') }) - }) + } - t.test('should properly name transaction with parameterized routes', async (t) => { - const { fastify, agent } = t.context + await t.test('should properly name transaction with parameterized routes', async (t) => { + const { fastify, agent } = t.nr + let txPassed = false agent.on('transactionFinished', (transaction) => { - t.equal( + assert.equal( transaction.name, 'WebTransaction/WebFrameworkUri/Fastify/GET//params/:id/:parent/edit' ) - t.equal(transaction.url, '/params/id/parent/edit') + assert.equal(transaction.url, '/params/id/parent/edit') + + txPassed = true }) + await fastify.listen() const address = fastify.server.address() const result = await makeRequest(address, '/params/id/parent/edit') - t.same(result, { id: 'id', parent: 'parent' }) - t.end() + match(result, { id: 'id', parent: 'parent' }) + + assert.equal(txPassed, true, 'transactionFinished assertions passed') }) } diff --git a/test/versioned/fastify/new-state-tracking.tap.js b/test/versioned/fastify/new-state-tracking.tap.js deleted file mode 100644 index d9eb76d856..0000000000 --- a/test/versioned/fastify/new-state-tracking.tap.js +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2021 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const tap = require('tap') -const helper = require('../../lib/agent_helper') -const common = require('./common') - -const originalSetImmediate = setImmediate - -tap.test('fastify with new state tracking', (t) => { - t.autoend() - - let agent = null - let fastify = null - - t.beforeEach(() => { - agent = helper.instrumentMockedAgent({ - feature_flag: { - new_promise_tracking: true - } - }) - - fastify = require('fastify')() - }) - - t.afterEach(() => { - helper.unloadAgent(agent) - fastify.close() - }) - - t.test('should not reuse transactions via normal usage', async (t) => { - fastify.get('/', async () => { - return { hello: 'world' } - }) - - await fastify.listen({ port: 0 }) - - const address = fastify.server.address() - - const transactions = [] - agent.on('transactionFinished', (transaction) => { - transactions.push(transaction) - }) - - await common.makeRequest(address, '/') - await common.makeRequest(address, '/') - - t.equal(transactions.length, 2) - }) - - t.test('should not reuse transactions with non-awaited promise', async (t) => { - fastify.get('/', async () => { - doWork() // fire-and-forget promise - return { hello: 'world' } - }) - - function doWork() { - return new Promise((resolve) => { - // async hop w/o context tracking - originalSetImmediate(resolve) - }) - } - - await fastify.listen({ port: 0 }) - - const address = fastify.server.address() - - const transactions = [] - agent.on('transactionFinished', (transaction) => { - transactions.push(transaction) - }) - - await common.makeRequest(address, '/') - await common.makeRequest(address, '/') - - t.equal(transactions.length, 2) - }) -}) diff --git a/test/versioned/fastify/new-state-tracking.test.js b/test/versioned/fastify/new-state-tracking.test.js new file mode 100644 index 0000000000..146bb21f6d --- /dev/null +++ b/test/versioned/fastify/new-state-tracking.test.js @@ -0,0 +1,77 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const test = require('node:test') +const assert = require('node:assert') + +const helper = require('../../lib/agent_helper') +const common = require('./common') + +test.beforeEach((ctx) => { + ctx.nr = {} + ctx.nr.agent = helper.instrumentMockedAgent({ feature_flag: { new_promise_tracking: true } }) + ctx.nr.fastify = require('fastify')() + ctx.nr.originalSetImmediate = global.setImmediate +}) + +test.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) + ctx.nr.fastify.close() + global.setImmediate = ctx.nr.originalSetImmediate +}) + +test('should not reuse transactions via normal usage', async (t) => { + const { agent, fastify } = t.nr + + fastify.get('/', async () => { + return { hello: 'world' } + }) + + await fastify.listen({ port: 0 }) + + const address = fastify.server.address() + + const transactions = [] + agent.on('transactionFinished', (transaction) => { + transactions.push(transaction) + }) + + await common.makeRequest(address, '/') + await common.makeRequest(address, '/') + + assert.equal(transactions.length, 2) +}) + +test('should not reuse transactions with non-awaited promise', async (t) => { + const { agent, fastify, originalSetImmediate } = t.nr + + fastify.get('/', async () => { + doWork() // fire-and-forget promise + return { hello: 'world' } + }) + + function doWork() { + return new Promise((resolve) => { + // async hop w/o context tracking + originalSetImmediate(resolve) + }) + } + + await fastify.listen({ port: 0 }) + + const address = fastify.server.address() + + const transactions = [] + agent.on('transactionFinished', (transaction) => { + transactions.push(transaction) + }) + + await common.makeRequest(address, '/') + await common.makeRequest(address, '/') + + assert.equal(transactions.length, 2) +}) diff --git a/test/versioned/fastify/package.json b/test/versioned/fastify/package.json index a088c18f66..2bfbecd34c 100644 --- a/test/versioned/fastify/package.json +++ b/test/versioned/fastify/package.json @@ -1,6 +1,11 @@ { "name": "fastify-tests", - "targets": [{"name":"fastify","minAgentVersion":"8.5.0"}], + "targets": [ + { + "name": "fastify", + "minAgentVersion": "8.5.0" + } + ], "version": "0.0.0", "private": true, "tests": [ @@ -12,34 +17,65 @@ "fastify": ">=2.0.0 < 3" }, "files": [ - "add-hook.tap.js", - "code-level-metrics-middleware.tap.js", - "code-level-metrics-hooks.tap.js", - "errors.tap.js", - "fastify-2-naming.tap.js", - "new-state-tracking.tap.js" + "add-hook.test.js", + "code-level-metrics-middleware.test.js", + "code-level-metrics-hooks.test.js", + "errors.test.js", + "fastify-2-naming.test.js", + "new-state-tracking.test.js" + ] + }, + + { + "engines": { + "node": ">=18" + }, + "dependencies": { + "fastify": "^3.0.0", + "middie": ">=3.0.0 && <7.0.0" + }, + "files": [ + "fastify-gte3.test.js", + "fastify-gte3-naming.test.js" ] }, + { "engines": { "node": ">=18" }, "dependencies": { - "fastify": ">=3.0.0", - "middie": "5.3.0" + "fastify": "^4.0.0", + "@fastify/middie": ">=7.0.0 && <9.0.0" }, "files": [ - "add-hook.tap.js", - "code-level-metrics-middleware.tap.js", - "code-level-metrics-hooks.tap.js", - "errors.tap.js", - "fastify-3.tap.js", - "fastify-3-naming.tap.js", - "new-state-tracking.tap.js" + "fastify-gte3.test.js", + "fastify-gte3-naming.test.js", + "add-hook.test.js", + "code-level-metrics-middleware.test.js", + "code-level-metrics-hooks.test.js", + "errors.test.js", + "new-state-tracking.test.js" + ] + }, + + { + "engines": { + "node": ">=18" + }, + "dependencies": { + "fastify": "^5.0.0", + "@fastify/middie": ">=9.0.0" + }, + "files": [ + "fastify-gte3.test.js", + "fastify-gte3-naming.test.js", + "add-hook.test.js", + "code-level-metrics-middleware.test.js", + "code-level-metrics-hooks.test.js", + "errors.test.js", + "new-state-tracking.test.js" ] } - ], - "engines": { - "node": ">=18" - } + ] }