diff --git a/test/versioned/hapi/capture-params.tap.js b/test/versioned/hapi/capture-params.tap.js deleted file mode 100644 index 157eb43a21..0000000000 --- a/test/versioned/hapi/capture-params.tap.js +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const DESTINATIONS = require('../../../lib/config/attribute-filter').DESTINATIONS -const tap = require('tap') -const helper = require('../../lib/agent_helper') -const utils = require('./hapi-utils') -const HTTP_ATTS = require('../../lib/fixtures').httpAttributes - -tap.test('Hapi capture params support', function (t) { - t.autoend() - - let agent = null - let server = null - let port = null - - t.beforeEach(function () { - agent = helper.instrumentMockedAgent({ - attributes: { - enabled: true, - include: ['request.parameters.*'] - } - }) - - server = utils.getServer() - - agent.config.attributes.enabled = true - }) - - t.afterEach(function () { - helper.unloadAgent(agent) - return server.stop() - }) - - t.test('simple case with no params', function (t) { - agent.on('transactionFinished', function (transaction) { - t.ok(transaction.trace, 'transaction has a trace.') - const attributes = transaction.trace.attributes.get(DESTINATIONS.TRANS_TRACE) - HTTP_ATTS.forEach(function (key) { - t.ok(attributes[key], 'Trace contains expected HTTP attribute: ' + key) - }) - }) - - server.route({ - method: 'GET', - path: '/test/', - handler: function () { - t.ok(agent.getTransaction(), 'transaction is available inside route handler') - return { status: 'ok' } - } - }) - - server.start().then(function () { - port = server.info.port - makeRequest(t, 'http://localhost:' + port + '/test/') - }) - }) - - t.test('case with route params', function (t) { - agent.on('transactionFinished', function (tx) { - t.ok(tx.trace, 'transaction has a trace.') - const attributes = tx.trace.attributes.get(DESTINATIONS.TRANS_TRACE) - t.equal( - attributes['request.parameters.route.id'], - '1337', - 'Trace attributes include `id` route param' - ) - }) - - server.route({ - method: 'GET', - path: '/test/{id}/', - handler: function () { - t.ok(agent.getTransaction(), 'transaction is available') - return { status: 'ok' } - } - }) - - server.start().then(function () { - port = server.info.port - makeRequest(t, 'http://localhost:' + port + '/test/1337/') - }) - }) - - t.test('case with query params', function (t) { - agent.on('transactionFinished', function (tx) { - t.ok(tx.trace, 'transaction has a trace.') - const attributes = tx.trace.attributes.get(DESTINATIONS.TRANS_TRACE) - t.equal( - attributes['request.parameters.name'], - 'hapi', - 'Trace attributes include `name` query param' - ) - }) - - server.route({ - method: 'GET', - path: '/test/', - handler: function () { - t.ok(agent.getTransaction(), 'transaction is available') - return { status: 'ok' } - } - }) - - server.start().then(function () { - port = server.info.port - makeRequest(t, 'http://localhost:' + port + '/test/?name=hapi') - }) - }) - - t.test('case with both route and query params', function (t) { - agent.on('transactionFinished', function (tx) { - t.ok(tx.trace, 'transaction has a trace.') - const attributes = tx.trace.attributes.get(DESTINATIONS.TRANS_TRACE) - t.equal( - attributes['request.parameters.route.id'], - '1337', - 'Trace attributes include `id` route param' - ) - t.equal( - attributes['request.parameters.name'], - 'hapi', - 'Trace attributes include `name` query param' - ) - }) - - server.route({ - method: 'GET', - path: '/test/{id}/', - handler: function () { - t.ok(agent.getTransaction(), 'transaction is available') - return { status: 'ok' } - } - }) - - server.start().then(function () { - port = server.info.port - makeRequest(t, 'http://localhost:' + port + '/test/1337/?name=hapi') - }) - }) -}) - -function makeRequest(t, uri) { - helper.makeGetRequest(uri, function (_err, res, body) { - t.equal(res.statusCode, 200, 'nothing exploded') - t.same(body, { status: 'ok' }, 'got expected response') - t.end() - }) -} diff --git a/test/versioned/hapi/capture-params.test.js b/test/versioned/hapi/capture-params.test.js new file mode 100644 index 0000000000..e4619d82c1 --- /dev/null +++ b/test/versioned/hapi/capture-params.test.js @@ -0,0 +1,162 @@ +/* + * 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 utils = require('./hapi-utils') + +const DESTINATIONS = require('../../../lib/config/attribute-filter').DESTINATIONS +const HTTP_ATTS = require('../../lib/fixtures').httpAttributes + +test.beforeEach((ctx) => { + ctx.nr = {} + ctx.nr.agent = helper.instrumentMockedAgent({ + attributes: { + enabled: true, + include: ['request.parameters.*'] + } + }) + + ctx.nr.server = utils.getServer() +}) + +test.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) + ctx.nr.server.stop() +}) + +function makeRequest(uri) { + helper.makeGetRequest(uri, {}, function (_err, res, body) { + assert.equal(res.statusCode, 200, 'nothing exploded') + assert.deepStrictEqual(body, { status: 'ok' }, 'got expected response') + }) +} + +test('simple case with no params', (t, end) => { + const { agent, server } = t.nr + + agent.on('transactionFinished', function (transaction) { + assert.ok(transaction.trace, 'transaction has a trace.') + const attributes = transaction.trace.attributes.get(DESTINATIONS.TRANS_TRACE) + HTTP_ATTS.forEach(function (key) { + assert.ok(attributes[key], 'Trace contains expected HTTP attribute: ' + key) + }) + + end() + }) + + server.route({ + method: 'GET', + path: '/test/', + handler: function () { + assert.ok(agent.getTransaction(), 'transaction is available inside route handler') + return { status: 'ok' } + } + }) + + server.start().then(function () { + const port = server.info.port + makeRequest('http://localhost:' + port + '/test/') + }) +}) + +test('case with route params', (t, end) => { + const { agent, server } = t.nr + + agent.on('transactionFinished', function (tx) { + assert.ok(tx.trace, 'transaction has a trace.') + const attributes = tx.trace.attributes.get(DESTINATIONS.TRANS_TRACE) + assert.equal( + attributes['request.parameters.route.id'], + '1337', + 'Trace attributes include `id` route param' + ) + + end() + }) + + server.route({ + method: 'GET', + path: '/test/{id}/', + handler: function () { + assert.ok(agent.getTransaction(), 'transaction is available') + return { status: 'ok' } + } + }) + + server.start().then(function () { + const port = server.info.port + makeRequest('http://localhost:' + port + '/test/1337/') + }) +}) + +test('case with query params', (t, end) => { + const { agent, server } = t.nr + + agent.on('transactionFinished', function (tx) { + assert.ok(tx.trace, 'transaction has a trace.') + const attributes = tx.trace.attributes.get(DESTINATIONS.TRANS_TRACE) + assert.equal( + attributes['request.parameters.name'], + 'hapi', + 'Trace attributes include `name` query param' + ) + + end() + }) + + server.route({ + method: 'GET', + path: '/test/', + handler: function () { + assert.ok(agent.getTransaction(), 'transaction is available') + return { status: 'ok' } + } + }) + + server.start().then(function () { + const port = server.info.port + makeRequest('http://localhost:' + port + '/test/?name=hapi') + }) +}) + +test('case with both route and query params', (t, end) => { + const { agent, server } = t.nr + + agent.on('transactionFinished', function (tx) { + assert.ok(tx.trace, 'transaction has a trace.') + const attributes = tx.trace.attributes.get(DESTINATIONS.TRANS_TRACE) + assert.equal( + attributes['request.parameters.route.id'], + '1337', + 'Trace attributes include `id` route param' + ) + assert.equal( + attributes['request.parameters.name'], + 'hapi', + 'Trace attributes include `name` query param' + ) + + end() + }) + + server.route({ + method: 'GET', + path: '/test/{id}/', + handler: function () { + assert.ok(agent.getTransaction(), 'transaction is available') + return { status: 'ok' } + } + }) + + server.start().then(function () { + const port = server.info.port + makeRequest('http://localhost:' + port + '/test/1337/?name=hapi') + }) +}) diff --git a/test/versioned/hapi/errors.tap.js b/test/versioned/hapi/errors.tap.js deleted file mode 100644 index 9a13575d3d..0000000000 --- a/test/versioned/hapi/errors.tap.js +++ /dev/null @@ -1,223 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const helper = require('../../lib/agent_helper') -const http = require('http') -const tap = require('tap') -const utils = require('./hapi-utils') - -let agent -let server -let port - -tap.test('Hapi error handling', function (t) { - t.autoend() - - t.beforeEach(function () { - agent = helper.instrumentMockedAgent() - - server = utils.getServer() - }) - - t.afterEach(function () { - helper.unloadAgent(agent) - return server.stop() - }) - - t.test('does not report error when handler returns a string', function (t) { - server.route({ - method: 'GET', - path: '/test', - handler: function () { - return 'ok' - } - }) - - runTest(t, function (errors, statusCode) { - t.equal(errors.length, 0, 'should have no errors') - t.equal(statusCode, 200, 'should have a 200 status code') - t.end() - }) - }) - - t.test('reports error when an instance of Error is returned', function (t) { - server.route({ - method: 'GET', - path: '/test', - handler: function () { - return Promise.reject(new Error('rejected promise error')) - } - }) - - runTest(t, function (errors, statusCode) { - t.equal(errors.length, 1, 'should have one error') - - t.equal(errors[0][2], 'rejected promise error', 'should have expected error message') - - t.equal(statusCode, 500, 'should have expected error code') - t.end() - }) - }) - - t.test('reports error when thrown from a route', function (t) { - server.route({ - method: 'GET', - path: '/test', - handler: function () { - throw new Error('thrown error') - } - }) - - runTest(t, function (errors, statusCode) { - t.equal(errors.length, 1, 'should have one error') - t.equal(errors[0][2], 'thrown error', 'should have expected error message') - t.equal(statusCode, 500, 'should have expected error code') - t.end() - }) - }) - - t.test('reports error when thrown from a middleware', function (t) { - server.ext('onRequest', function () { - throw new Error('middleware error') - }) - - server.route({ - method: 'GET', - path: '/test', - handler: function () { - return 'ok' - } - }) - - runTest(t, function (errors, statusCode) { - t.equal(errors.length, 1, 'should have one error') - t.equal(errors[0][2], 'middleware error', 'should have expected error message') - t.equal(statusCode, 500, 'should have expected error code') - t.end() - }) - }) - - t.test('reports error when error handler replies with transformed error', (t) => { - server.ext('onPreResponse', (req) => { - t.ok(req.response instanceof Error, 'preResponse has error') - req.response.output.statusCode = 400 - return req.response - }) - - server.route({ - method: 'GET', - path: '/test', - handler: () => { - throw new Error('route handler error') - } - }) - - runTest(t, (errors, statusCode) => { - t.equal(errors.length, 1, 'has 1 reported error') - t.equal(errors[0][2], 'route handler error', 'has correct error message') - t.equal(statusCode, 400, 'has expected 400 status code') - t.end() - }) - }) - - t.test('reports error when error handler continues with transformed response', (t) => { - server.ext('onPreResponse', (req, h) => { - t.ok(req.response instanceof Error, 'preResponse has error') - req.response.output.statusCode = 400 - return h.continue - }) - - server.route({ - method: 'GET', - path: '/test', - handler: () => { - throw new Error('route handler error') - } - }) - - runTest(t, (errors, statusCode) => { - t.equal(errors.length, 1, 'has 1 reported error') - t.equal(errors[0][2], 'route handler error', 'has correct error message') - t.equal(statusCode, 400, 'has expected 400 status code') - t.end() - }) - }) - - t.test('reports error when error handler continues with original response', (t) => { - server.ext('onPreResponse', (req, h) => { - t.ok(req.response instanceof Error, 'preResponse has error') - return h.continue - }) - - server.route({ - method: 'GET', - path: '/test', - handler: () => { - throw new Error('route handler error') - } - }) - - runTest(t, (errors, statusCode) => { - t.equal(errors.length, 1, 'has 1 reported error') - t.equal(errors[0][2], 'route handler error', 'has correct error message') - t.equal(statusCode, 500, 'has expected 500 status code') - t.end() - }) - }) - - t.test('should not report error when error handler responds', (t) => { - server.ext('onPreResponse', (req) => { - t.ok(req.response.isBoom, 'preResponse has error') - return null - }) - - server.route({ - method: 'GET', - path: '/test', - handler: () => { - throw new Error('route handler error') - } - }) - - runTest(t, (errors, statusCode) => { - t.equal(errors.length, 0, 'has no reported errors') - t.ok([200, 204].includes(statusCode), 'has expected 200 or 204 status code') - t.end() - }) - }) -}) - -function runTest(t, callback) { - let statusCode - let errors - - agent.on('transactionFinished', function () { - errors = agent.errors.traceAggregator.errors - if (statusCode) { - callback(errors, statusCode) - } - }) - - const endpoint = '/test' - server.start().then(function () { - port = server.info.port - makeRequest(endpoint, function (response) { - statusCode = response.statusCode - if (errors) { - callback(errors, statusCode) - } - response.resume() - }) - }) - t.teardown(function () { - server.stop() - }) -} - -function makeRequest(path, callback) { - http.request({ port: port, path: path }, callback).end() -} diff --git a/test/versioned/hapi/errors.test.js b/test/versioned/hapi/errors.test.js new file mode 100644 index 0000000000..716834c366 --- /dev/null +++ b/test/versioned/hapi/errors.test.js @@ -0,0 +1,228 @@ +/* + * 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 http = require('node:http') + +const helper = require('../../lib/agent_helper') +const utils = require('./hapi-utils') + +test.beforeEach((ctx) => { + ctx.nr = {} + ctx.nr.agent = helper.instrumentMockedAgent() + + ctx.nr.server = utils.getServer() +}) + +test.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) + ctx.nr.server.stop() +}) + +function runTest(agent, server, callback) { + let statusCode + let errors + + agent.on('transactionFinished', function () { + errors = agent.errors.traceAggregator.errors + if (statusCode) { + callback(errors, statusCode) + } + }) + + const endpoint = '/test' + server.start().then(function () { + makeRequest(endpoint, server.info.port, function (response) { + statusCode = response.statusCode + if (errors) { + callback(errors, statusCode) + } + response.resume() + }) + }) +} + +function makeRequest(path, port, callback) { + http.request({ port, path }, callback).end() +} + +test('does not report error when handler returns a string', (t, end) => { + const { agent, server } = t.nr + + server.route({ + method: 'GET', + path: '/test', + handler: function () { + return 'ok' + } + }) + + runTest(agent, server, function (errors, statusCode) { + assert.equal(errors.length, 0, 'should have no errors') + assert.equal(statusCode, 200, 'should have a 200 status code') + end() + }) +}) + +test('reports error when an instance of Error is returned', (t, end) => { + const { agent, server } = t.nr + + server.route({ + method: 'GET', + path: '/test', + handler: function () { + return Promise.reject(new Error('rejected promise error')) + } + }) + + runTest(agent, server, function (errors, statusCode) { + assert.equal(errors.length, 1, 'should have one error') + assert.equal(errors[0][2], 'rejected promise error', 'should have expected error message') + assert.equal(statusCode, 500, 'should have expected error code') + end() + }) +}) + +test('reports error when thrown from a route', (t, end) => { + const { agent, server } = t.nr + + server.route({ + method: 'GET', + path: '/test', + handler: function () { + throw new Error('thrown error') + } + }) + + runTest(agent, server, function (errors, statusCode) { + assert.equal(errors.length, 1, 'should have one error') + assert.equal(errors[0][2], 'thrown error', 'should have expected error message') + assert.equal(statusCode, 500, 'should have expected error code') + end() + }) +}) + +test('reports error when thrown from a middleware', (t, end) => { + const { agent, server } = t.nr + + server.ext('onRequest', function () { + throw new Error('middleware error') + }) + + server.route({ + method: 'GET', + path: '/test', + handler: function () { + return 'ok' + } + }) + + runTest(agent, server, function (errors, statusCode) { + assert.equal(errors.length, 1, 'should have one error') + assert.equal(errors[0][2], 'middleware error', 'should have expected error message') + assert.equal(statusCode, 500, 'should have expected error code') + end() + }) +}) + +test('reports error when error handler replies with transformed error', (t, end) => { + const { agent, server } = t.nr + + server.ext('onPreResponse', (req) => { + assert.ok(req.response instanceof Error, 'preResponse has error') + req.response.output.statusCode = 400 + return req.response + }) + + server.route({ + method: 'GET', + path: '/test', + handler: () => { + throw new Error('route handler error') + } + }) + + runTest(agent, server, (errors, statusCode) => { + assert.equal(errors.length, 1, 'has 1 reported error') + assert.equal(errors[0][2], 'route handler error', 'has correct error message') + assert.equal(statusCode, 400, 'has expected 400 status code') + end() + }) +}) + +test('reports error when error handler continues with transformed response', (t, end) => { + const { agent, server } = t.nr + + server.ext('onPreResponse', (req, h) => { + assert.ok(req.response instanceof Error, 'preResponse has error') + req.response.output.statusCode = 400 + return h.continue + }) + + server.route({ + method: 'GET', + path: '/test', + handler: () => { + throw new Error('route handler error') + } + }) + + runTest(agent, server, (errors, statusCode) => { + assert.equal(errors.length, 1, 'has 1 reported error') + assert.equal(errors[0][2], 'route handler error', 'has correct error message') + assert.equal(statusCode, 400, 'has expected 400 status code') + end() + }) +}) + +test('reports error when error handler continues with original response', (t, end) => { + const { agent, server } = t.nr + + server.ext('onPreResponse', (req, h) => { + assert.ok(req.response instanceof Error, 'preResponse has error') + return h.continue + }) + + server.route({ + method: 'GET', + path: '/test', + handler: () => { + throw new Error('route handler error') + } + }) + + runTest(agent, server, (errors, statusCode) => { + assert.equal(errors.length, 1, 'has 1 reported error') + assert.equal(errors[0][2], 'route handler error', 'has correct error message') + assert.equal(statusCode, 500, 'has expected 500 status code') + end() + }) +}) + +test('should not report error when error handler responds', (t, end) => { + const { agent, server } = t.nr + + server.ext('onPreResponse', (req) => { + assert.ok(req.response.isBoom, 'preResponse has error') + return null + }) + + server.route({ + method: 'GET', + path: '/test', + handler: () => { + throw new Error('route handler error') + } + }) + + runTest(agent, server, (errors, statusCode) => { + assert.equal(errors.length, 0, 'has no reported errors') + assert.ok([200, 204].includes(statusCode), 'has expected 200 or 204 status code') + end() + }) +}) diff --git a/test/versioned/hapi/ext.tap.js b/test/versioned/hapi/ext.tap.js deleted file mode 100644 index 3c3872d878..0000000000 --- a/test/versioned/hapi/ext.tap.js +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright 2020 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 utils = require('./hapi-utils') - -tap.test('Hapi ext', function (t) { - t.autoend() - - let agent = null - let server = null - let port = null - - // Queue that executes outside of a transaction context - const tasks = [] - const intervalId = setInterval(function () { - while (tasks.length) { - const task = tasks.pop() - task() - } - }, 10) - function resolveOutOfScope(val) { - return new Promise(function (resolve) { - tasks.push(function () { - resolve(val) - }) - }) - } - - t.teardown(function () { - clearInterval(intervalId) - }) - - t.beforeEach(function () { - agent = helper.instrumentMockedAgent() - - server = utils.getServer() - }) - - t.afterEach(function () { - helper.unloadAgent(agent) - return server.stop() - }) - - t.test('keeps context with a single handler', function (t) { - server.ext('onRequest', function (req, h) { - t.ok(agent.getTransaction(), 'transaction is available in onRequest handler') - return resolveOutOfScope(h.continue) - }) - - addRouteAndGet(t) - }) - - t.test('keeps context with a handler object with a single method', function (t) { - server.ext({ - type: 'onRequest', - method: function (req, h) { - t.ok(agent.getTransaction(), 'transaction is available in onRequest handler') - return resolveOutOfScope(h.continue) - } - }) - - addRouteAndGet(t) - }) - - t.test('keeps context with a handler object with an array of methods', function (t) { - server.ext({ - type: 'onRequest', - method: [ - function (req, h) { - t.ok(agent.getTransaction(), 'transaction is available in first handler') - return resolveOutOfScope(h.continue) - }, - function (req, h) { - t.ok(agent.getTransaction(), 'transaction is available in second handler') - return Promise.resolve(h.continue) - } - ] - }) - - addRouteAndGet(t) - }) - - t.test('keeps context with an array of handlers and an array of methods', function (t) { - server.ext([ - { - type: 'onRequest', - method: [ - function (req, h) { - t.ok(agent.getTransaction(), 'transaction is available in first handler') - return resolveOutOfScope(h.continue) - }, - function (req, h) { - t.ok(agent.getTransaction(), 'transaction is available in second handler') - return Promise.resolve(h.continue) - } - ] - }, - { - type: 'onPreHandler', - method: function (req, h) { - t.ok(agent.getTransaction(), 'transaction is available in third handler') - return resolveOutOfScope(h.continue) - } - } - ]) - - addRouteAndGet(t) - }) - - t.test('does not crash on non-request events', function (t) { - server.ext('onPreStart', function (s) { - t.notOk(agent.getTransaction(), 'should not have transaction in server events') - t.equal(s, server, 'should pass through arguments without change') - return Promise.resolve() - }) - - addRouteAndGet(t) - }) - - function addRouteAndGet(t) { - server.route({ - method: 'GET', - path: '/test', - handler: function myHandler() { - t.ok(agent.getTransaction(), 'transaction is available in route handler') - return 'ok' - } - }) - - server - .start() - .then(function () { - port = server.info.port - helper.makeGetRequest('http://localhost:' + port + '/test', function () { - t.end() - }) - }) - .catch(function (err) { - t.error(err, 'should not fail to start server and request') - t.end() - }) - } -}) diff --git a/test/versioned/hapi/ext.test.js b/test/versioned/hapi/ext.test.js new file mode 100644 index 0000000000..aa03b93739 --- /dev/null +++ b/test/versioned/hapi/ext.test.js @@ -0,0 +1,160 @@ +/* + * 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 utils = require('./hapi-utils') + +// Queue that executes outside of a transaction context +const tasks = [] +const intervalId = setInterval(function () { + while (tasks.length) { + const task = tasks.pop() + task() + } +}, 10) + +function addRouteAndGet(ctx, end) { + const { agent, server } = ctx + server.route({ + method: 'GET', + path: '/test', + handler: function myHandler() { + assert.ok(agent.getTransaction(), 'transaction is available in route handler') + return 'ok' + } + }) + + server + .start() + .then(function () { + const port = server.info.port + helper.makeGetRequest('http://localhost:' + port + '/test', function () { + end() + }) + }) + .catch(function (err) { + assert.ifError(err, 'should not fail to start server and request') + end() + }) +} + +function resolveOutOfScope(val) { + return new Promise(function (resolve) { + tasks.push(function () { + resolve(val) + }) + }) +} + +test.beforeEach((ctx) => { + ctx.nr = {} + ctx.nr.agent = helper.instrumentMockedAgent({ + attributes: { + enabled: true, + include: ['request.parameters.*'] + } + }) + + ctx.nr.server = utils.getServer() +}) + +test.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) + ctx.nr.server.stop() +}) + +test.after(() => { + clearInterval(intervalId) +}) + +test('keeps context with a single handler', (t, end) => { + const { agent, server } = t.nr + server.ext('onRequest', function (req, h) { + assert.ok(agent.getTransaction(), 'transaction is available in onRequest handler') + return resolveOutOfScope(h.continue) + }) + + addRouteAndGet(t.nr, end) +}) + +test('keeps context with a handler object with a single method', (t, end) => { + const { agent, server } = t.nr + + server.ext({ + type: 'onRequest', + method: function (req, h) { + assert.ok(agent.getTransaction(), 'transaction is available in onRequest handler') + return resolveOutOfScope(h.continue) + } + }) + + addRouteAndGet(t.nr, end) +}) + +test('keeps context with a handler object with an array of methods', (t, end) => { + const { agent, server } = t.nr + + server.ext({ + type: 'onRequest', + method: [ + function (req, h) { + assert.ok(agent.getTransaction(), 'transaction is available in first handler') + return resolveOutOfScope(h.continue) + }, + function (req, h) { + assert.ok(agent.getTransaction(), 'transaction is available in second handler') + return Promise.resolve(h.continue) + } + ] + }) + + addRouteAndGet(t.nr, end) +}) + +test('keeps context with an array of handlers and an array of methods', (t, end) => { + const { agent, server } = t.nr + + server.ext([ + { + type: 'onRequest', + method: [ + function (req, h) { + assert.ok(agent.getTransaction(), 'transaction is available in first handler') + return resolveOutOfScope(h.continue) + }, + function (req, h) { + assert.ok(agent.getTransaction(), 'transaction is available in second handler') + return Promise.resolve(h.continue) + } + ] + }, + { + type: 'onPreHandler', + method: function (req, h) { + assert.ok(agent.getTransaction(), 'transaction is available in third handler') + return resolveOutOfScope(h.continue) + } + } + ]) + + addRouteAndGet(t.nr, end) +}) + +test('does not crash on non-request events', (t, end) => { + const { agent, server } = t.nr + + server.ext('onPreStart', function (s) { + assert.equal(agent.getTransaction(), undefined, 'should not have transaction in server events') + assert.equal(s, server, 'should pass through arguments without change') + return Promise.resolve() + }) + + addRouteAndGet(t.nr, end) +}) diff --git a/test/versioned/hapi/hapi-utils.js b/test/versioned/hapi/hapi-utils.js index bff98de8cf..789fcce202 100644 --- a/test/versioned/hapi/hapi-utils.js +++ b/test/versioned/hapi/hapi-utils.js @@ -5,8 +5,6 @@ 'use strict' -const tap = require('tap') - exports.getServer = function getServer(cfg) { cfg = cfg || {} const host = cfg.host || 'localhost' @@ -19,6 +17,5 @@ exports.getServer = function getServer(cfg) { const servers = ['Server', 'server'] const server = servers[Math.round(Math.random())] - tap.comment(`Randomly testing with hapi.${server}`) return hapi[server](Object.assign({}, opts, { host, port })) } diff --git a/test/versioned/hapi/hapi.tap.js b/test/versioned/hapi/hapi.tap.js deleted file mode 100644 index bcc1c8bdfd..0000000000 --- a/test/versioned/hapi/hapi.tap.js +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const tap = require('tap') -const shims = require('../../../lib/shim') -const helper = require('../../lib/agent_helper') -const instrument = require('../../../lib/instrumentation/@hapi/hapi') -const utils = require('./hapi-utils') - -tap.test('instrumentation of Hapi', function (t) { - t.autoend() - - t.test('preserves server creation return', function (t) { - const agent = helper.loadMockedAgent() - const hapi = require('@hapi/hapi') - const returned = utils.getServer({ hapi: hapi }) - - t.ok(returned != null, 'Hapi returns from server creation') - - const shim = new shims.WebFrameworkShim(agent, 'hapi') - instrument(agent, hapi, 'hapi', shim) - - const returned2 = utils.getServer({ hapi: hapi }) - - t.ok(returned2 != null, 'Server creation returns when instrumented') - - t.end() - }) -}) diff --git a/test/versioned/hapi/hapi.test.js b/test/versioned/hapi/hapi.test.js new file mode 100644 index 0000000000..0270c0cacd --- /dev/null +++ b/test/versioned/hapi/hapi.test.js @@ -0,0 +1,42 @@ +/* + * 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 instrument = require('../../../lib/instrumentation/@hapi/hapi') +const shims = require('../../../lib/shim') +const helper = require('../../lib/agent_helper') +const utils = require('./hapi-utils') + +test.beforeEach((ctx) => { + ctx.nr = {} + ctx.nr.agent = helper.instrumentMockedAgent() + + ctx.nr.server = utils.getServer() +}) + +test.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) + ctx.nr.server.stop() +}) + +test('preserves server creation return', (t) => { + const { agent } = t.nr + + const hapi = require('@hapi/hapi') + const returned = utils.getServer({ hapi: hapi }) + + assert.ok(returned != null, 'Hapi returns from server creation') + + const shim = new shims.WebFrameworkShim(agent, 'hapi') + instrument(agent, hapi, 'hapi', shim) + + const returned2 = utils.getServer({ hapi: hapi }) + + assert.ok(returned2 != null, 'Server creation returns when instrumented') +}) diff --git a/test/versioned/hapi/ignoring.tap.js b/test/versioned/hapi/ignoring.test.js similarity index 66% rename from test/versioned/hapi/ignoring.tap.js rename to test/versioned/hapi/ignoring.test.js index 58cdbe0443..5bd90f7e54 100644 --- a/test/versioned/hapi/ignoring.tap.js +++ b/test/versioned/hapi/ignoring.test.js @@ -1,43 +1,45 @@ /* - * Copyright 2020 New Relic Corporation. All rights reserved. + * Copyright 2024 New Relic Corporation. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ 'use strict' -const test = require('tap').test -const helper = require('../../lib/agent_helper') +const test = require('node:test') +const tspl = require('@matteo.collina/tspl') + const API = require('../../../api') +const helper = require('../../lib/agent_helper') const utils = require('./hapi-utils') -test('ignoring a Hapi route', function (t) { - t.plan(6) +test('ignoring a Hapi route', async (t) => { + const plan = tspl(t, { plan: 6 }) const agent = helper.instrumentMockedAgent() const api = new API(agent) const server = utils.getServer() - t.teardown(function () { + t.after(function () { helper.unloadAgent(agent) return server.stop() }) agent.on('transactionFinished', function (transaction) { - t.ok(transaction.ignore, 'transaction is ignored') + plan.ok(transaction.ignore, 'transaction is ignored') - t.notOk(agent.traces.trace, 'should have no transaction trace') + plan.equal(agent.traces.trace, undefined, 'should have no transaction trace') const metrics = agent.metrics._metrics.unscoped // loading k2 adds instrumentation metrics for packages it instruments const expectedMetrics = helper.isSecurityAgentEnabled(agent) ? 11 : 3 - t.equal( + plan.equal( Object.keys(metrics).length, expectedMetrics, 'only supportability metrics added to agent collection' ) const errors = agent.errors.traceAggregator.errors - t.equal(errors.length, 0, 'no errors noticed') + plan.equal(errors.length, 0, 'no errors noticed') }) server.route({ @@ -53,8 +55,10 @@ test('ignoring a Hapi route', function (t) { const port = server.info.port const uri = 'http://localhost:' + port + '/order/31337' helper.makeGetRequest(uri, function (_error, res, body) { - t.equal(res.statusCode, 400, 'got expected error') - t.same(body, { status: 'cartcartcart' }, 'got expected response') + plan.equal(res.statusCode, 400, 'got expected error') + plan.deepStrictEqual(body, { status: 'cartcartcart' }, 'got expected response') }) }) + + await plan.completed }) diff --git a/test/versioned/hapi/package.json b/test/versioned/hapi/package.json index b184661593..08d296b030 100644 --- a/test/versioned/hapi/package.json +++ b/test/versioned/hapi/package.json @@ -14,16 +14,16 @@ "@hapi/vision": "latest" }, "files": [ - "capture-params.tap.js", - "errors.tap.js", - "ext.tap.js", - "hapi.tap.js", - "ignoring.tap.js", - "plugins.tap.js", - "render.tap.js", - "router.tap.js", - "segments.tap.js", - "vhost.tap.js" + "capture-params.test.js", + "errors.test.js", + "ext.test.js", + "hapi.test.js", + "ignoring.test.js", + "plugins.test.js", + "render.test.js", + "router.test.js", + "segments.test.js", + "vhost.test.js" ] } ] diff --git a/test/versioned/hapi/plugins.tap.js b/test/versioned/hapi/plugins.tap.js deleted file mode 100644 index cbb5b5ec26..0000000000 --- a/test/versioned/hapi/plugins.tap.js +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const helper = require('../../lib/agent_helper') -const tap = require('tap') -const utils = require('./hapi-utils') - -tap.test('Hapi Plugins', function (t) { - t.autoend() - - let agent = null - let server = null - let port = null - - // queue that executes outside of a transaction context - const tasks = [] - const intervalId = setInterval(function () { - while (tasks.length) { - const task = tasks.pop() - task() - } - }, 10) - - t.teardown(function () { - clearInterval(intervalId) - }) - - t.beforeEach(function () { - agent = helper.instrumentMockedAgent() - - server = utils.getServer() - }) - - t.afterEach(function () { - helper.unloadAgent(agent) - return server.stop() - }) - - t.test('maintains transaction state', function (t) { - t.plan(3) - - const plugin = { - register: function (srvr) { - srvr.route({ - method: 'GET', - path: '/test', - handler: function myHandler() { - t.ok(agent.getTransaction(), 'transaction is available') - return Promise.resolve('hello') - } - }) - }, - name: 'foobar' - } - - agent.on('transactionFinished', function (tx) { - t.equal( - tx.getFullName(), - 'WebTransaction/Hapi/GET//test', - 'should name transaction correctly' - ) - }) - - server - .register(plugin) - .then(function () { - return server.start() - }) - .then(function () { - port = server.info.port - helper.makeGetRequest('http://localhost:' + port + '/test', function (_error, _res, body) { - t.equal(body, 'hello', 'should not interfere with response') - }) - }) - }) - - t.test('includes route prefix in transaction name', function (t) { - t.plan(3) - - const plugin = { - register: function (srvr) { - srvr.route({ - method: 'GET', - path: '/test', - handler: function myHandler() { - t.ok(agent.getTransaction(), 'transaction is available') - return Promise.resolve('hello') - } - }) - }, - name: 'foobar' - } - - agent.on('transactionFinished', function (tx) { - t.equal( - tx.getFullName(), - 'WebTransaction/Hapi/GET//prefix/test', - 'should name transaction correctly' - ) - }) - - server - .register(plugin, { routes: { prefix: '/prefix' } }) - .then(function () { - return server.start() - }) - .then(function () { - port = server.info.port - helper.makeGetRequest( - 'http://localhost:' + port + '/prefix/test', - function (_error, _res, body) { - t.equal(body, 'hello', 'should not interfere with response') - } - ) - }) - }) -}) diff --git a/test/versioned/hapi/plugins.test.js b/test/versioned/hapi/plugins.test.js new file mode 100644 index 0000000000..1abe807006 --- /dev/null +++ b/test/versioned/hapi/plugins.test.js @@ -0,0 +1,114 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const test = require('node:test') +const tspl = require('@matteo.collina/tspl') + +const helper = require('../../lib/agent_helper') +const utils = require('./hapi-utils') + +test.beforeEach((ctx) => { + ctx.nr = {} + ctx.nr.agent = helper.instrumentMockedAgent() + + ctx.nr.server = utils.getServer() +}) + +test.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) + ctx.nr.server.stop() +}) + +test('maintains transaction state', async (t) => { + const plan = tspl(t, { plan: 3 }) + const { agent, server } = t.nr + + const plugin = { + register: function (srvr) { + srvr.route({ + method: 'GET', + path: '/test', + handler: function myHandler() { + plan.ok(agent.getTransaction(), 'transaction is available') + return Promise.resolve('hello') + } + }) + }, + name: 'foobar' + } + + agent.on('transactionFinished', function (tx) { + plan.equal( + tx.getFullName(), + 'WebTransaction/Hapi/GET//test', + 'should name transaction correctly' + ) + }) + + server + .register(plugin) + .then(function () { + return server.start() + }) + .then(function () { + const port = server.info.port + helper.makeGetRequest( + 'http://localhost:' + port + '/test', + {}, + function (_error, _res, body) { + plan.equal(body, 'hello', 'should not interfere with response') + } + ) + }) + + await plan.completed +}) + +test('includes route prefix in transaction name', async (t) => { + const plan = tspl(t, { plan: 3 }) + const { agent, server } = t.nr + + const plugin = { + register: function (srvr) { + srvr.route({ + method: 'GET', + path: '/test', + handler: function myHandler() { + plan.ok(agent.getTransaction(), 'transaction is available') + return Promise.resolve('hello') + } + }) + }, + name: 'foobar' + } + + agent.on('transactionFinished', function (tx) { + plan.equal( + tx.getFullName(), + 'WebTransaction/Hapi/GET//prefix/test', + 'should name transaction correctly' + ) + }) + + server + .register(plugin, { routes: { prefix: '/prefix' } }) + .then(function () { + return server.start() + }) + .then(function () { + const port = server.info.port + helper.makeGetRequest( + 'http://localhost:' + port + '/prefix/test', + {}, + function (_error, _res, body) { + plan.equal(body, 'hello', 'should not interfere with response') + } + ) + }) + + await plan.completed +}) diff --git a/test/versioned/hapi/render.tap.js b/test/versioned/hapi/render.tap.js deleted file mode 100644 index 8fafbbf004..0000000000 --- a/test/versioned/hapi/render.tap.js +++ /dev/null @@ -1,227 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const util = require('util') -const path = require('path') -const tap = require('tap') -const helper = require('../../lib/agent_helper') -const API = require('../../../api') -const utils = require('./hapi-utils') -const fixtures = require('./fixtures') - -tap.test('agent instrumentation of Hapi', function (t) { - t.autoend() - - let agent = null - let server = null - let port = null - - t.beforeEach(function () { - agent = helper.instrumentMockedAgent() - - server = utils.getServer() - }) - - t.afterEach(function () { - helper.unloadAgent(agent) - return server.stop() - }) - - t.test('for a normal request', { timeout: 5000 }, function (t) { - // set apdexT so apdex stats will be recorded - agent.config.apdex_t = 1 - - server.route({ - method: 'GET', - path: '/test', - handler: function () { - return { yep: true } - } - }) - - server.start().then(function () { - port = server.info.port - helper.makeGetRequest('http://localhost:' + port + '/test', function (error, response, body) { - t.error(error, 'should not fail to make request') - - t.ok(/application\/json/.test(response.headers['content-type']), 'got correct content type') - t.same(body, { yep: true }, 'response survived') - - let stats - - stats = agent.metrics.getMetric('WebTransaction/Hapi/GET//test') - t.ok(stats, 'found unscoped stats for request path') - t.equal(stats.callCount, 1, '/test was only requested once') - - stats = agent.metrics.getOrCreateApdexMetric('Apdex/Hapi/GET//test') - t.ok(stats, 'found apdex stats for request path') - t.equal(stats.satisfying, 1, 'got satisfactory response time') - t.equal(stats.tolerating, 0, 'got no tolerable requests') - t.equal(stats.frustrating, 0, 'got no frustrating requests') - - stats = agent.metrics.getMetric('WebTransaction') - t.ok(stats, 'found roll-up statistics for web requests') - t.equal(stats.callCount, 1, 'only one web request was made') - - stats = agent.metrics.getMetric('HttpDispatcher') - t.ok(stats, 'found HTTP dispatcher statistics') - t.equal(stats.callCount, 1, 'only one HTTP-dispatched request was made') - - const serialized = JSON.stringify(agent.metrics._toPayloadSync()) - t.ok( - serialized.match(/WebTransaction\/Hapi\/GET\/\/test/), - 'serialized metrics as expected' - ) - - t.end() - }) - }) - }) - - t.test('using EJS templates', { timeout: 2000 }, function (t) { - server.route({ - method: 'GET', - path: '/test', - handler: function (req, h) { - return h.view('index', { title: 'yo dawg' }) - } - }) - - agent.once('transactionFinished', function (tx) { - const stats = agent.metrics.getMetric('View/index/Rendering') - t.ok(stats, 'View metric should exist') - t.equal(stats.callCount, 1, 'should note the view rendering') - verifyEnded(tx.trace.root, tx) - }) - - function verifyEnded(root, tx) { - for (let i = 0, len = root.children.length; i < len; i++) { - const segment = root.children[i] - t.ok(segment.timer.hasEnd(), util.format('verify %s (%s) has ended', segment.name, tx.id)) - if (segment.children) { - verifyEnded(segment, tx) - } - } - } - - server - .register(require('@hapi/vision')) - .then(function () { - server.views({ - path: path.join(__dirname, './views'), - engines: { - ejs: require('ejs') - } - }) - return server.start() - }) - .then(function () { - port = server.info.port - helper.makeGetRequest( - 'http://localhost:' + port + '/test', - function (error, response, body) { - t.error(error) - t.equal(response.statusCode, 200, 'response code should be 200') - t.equal(body, fixtures.htmlBody, 'template should still render fine') - - t.end() - } - ) - }) - }) - - t.test('should generate rum headers', { timeout: 1000 }, function (t) { - const api = new API(agent) - - agent.config.application_id = '12345' - agent.config.browser_monitoring.browser_key = '12345' - agent.config.browser_monitoring.js_agent_loader = 'function(){}' - - server.route({ - method: 'GET', - path: '/test', - handler: function (req, h) { - const rum = api.getBrowserTimingHeader() - t.equal(rum.substring(0, 7), ' { + ctx.nr = {} + ctx.nr.agent = helper.instrumentMockedAgent() + + ctx.nr.server = utils.getServer() +}) + +test.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) + ctx.nr.server.stop() +}) + +test('for a normal request', { timeout: 5000 }, (t, end) => { + const { agent, server } = t.nr + + // set apdexT so apdex stats will be recorded + agent.config.apdex_t = 1 + + server.route({ + method: 'GET', + path: '/test', + handler: function () { + return { yep: true } + } + }) + + server.start().then(function () { + const port = server.info.port + helper.makeGetRequest('http://localhost:' + port + '/test', function (error, response, body) { + assert.ifError(error, 'should not fail to make request') + + assert.ok( + /application\/json/.test(response.headers['content-type']), + 'got correct content type' + ) + assert.deepStrictEqual(body, { yep: true }, 'response survived') + + let stats + + stats = agent.metrics.getMetric('WebTransaction/Hapi/GET//test') + assert.ok(stats, 'found unscoped stats for request path') + assert.equal(stats.callCount, 1, '/test was only requested once') + + stats = agent.metrics.getOrCreateApdexMetric('Apdex/Hapi/GET//test') + assert.ok(stats, 'found apdex stats for request path') + assert.equal(stats.satisfying, 1, 'got satisfactory response time') + assert.equal(stats.tolerating, 0, 'got no tolerable requests') + assert.equal(stats.frustrating, 0, 'got no frustrating requests') + + stats = agent.metrics.getMetric('WebTransaction') + assert.ok(stats, 'found roll-up statistics for web requests') + assert.equal(stats.callCount, 1, 'only one web request was made') + + stats = agent.metrics.getMetric('HttpDispatcher') + assert.ok(stats, 'found HTTP dispatcher statistics') + assert.equal(stats.callCount, 1, 'only one HTTP-dispatched request was made') + + const serialized = JSON.stringify(agent.metrics._toPayloadSync()) + assert.ok( + serialized.match(/WebTransaction\/Hapi\/GET\/\/test/), + 'serialized metrics as expected' + ) + + end() + }) + }) +}) + +test('using EJS templates', { timeout: 2000 }, (t, end) => { + const { agent, server } = t.nr + + server.route({ + method: 'GET', + path: '/test', + handler: function (req, h) { + return h.view('index', { title: 'yo dawg' }) + } + }) + + agent.once('transactionFinished', function (tx) { + const stats = agent.metrics.getMetric('View/index/Rendering') + assert.ok(stats, 'View metric should exist') + assert.equal(stats.callCount, 1, 'should note the view rendering') + verifyEnded(tx.trace.root, tx) + }) + + function verifyEnded(root, tx) { + for (let i = 0, len = root.children.length; i < len; i++) { + const segment = root.children[i] + assert.ok( + segment.timer.hasEnd(), + util.format('verify %s (%s) has ended', segment.name, tx.id) + ) + if (segment.children) { + verifyEnded(segment, tx) + } + } + } + + server + .register(require('@hapi/vision')) + .then(function () { + server.views({ + path: path.join(__dirname, './views'), + engines: { + ejs: require('ejs') + } + }) + return server.start() + }) + .then(function () { + const port = server.info.port + helper.makeGetRequest('http://localhost:' + port + '/test', function (error, response, body) { + assert.ifError(error) + assert.equal(response.statusCode, 200, 'response code should be 200') + assert.equal(body, fixtures.htmlBody, 'template should still render fine') + + end() + }) + }) +}) + +test('should generate rum headers', { timeout: 1000 }, (t, end) => { + const { agent, server } = t.nr + const api = new API(agent) + + agent.config.application_id = '12345' + agent.config.browser_monitoring.browser_key = '12345' + agent.config.browser_monitoring.js_agent_loader = 'function(){}' + + server.route({ + method: 'GET', + path: '/test', + handler: function (req, h) { + const rum = api.getBrowserTimingHeader() + assert.equal(rum.substring(0, 7), ' { + const { agent } = t.nr + const server = utils.getServer({ options: { debug: false } }) + + t.after(() => server.stop()) + + agent.on('transactionFinished', function (tx) { + assert.equal( + tx.name, + 'WebTransaction/Hapi/GET/' + '/test', + 'Transaction should be named correctly.' + ) + }) + + server.route({ + method: 'GET', + path: '/test', + handler: function () { + let hmm + hmm.ohno.failure.is.terrible() + } + }) + + server.start().then(function () { + const port = server.info.port + helper.makeGetRequest( + 'http://localhost:' + port + '/test', + {}, + function (error, response, body) { + assert.ifError(error) + + assert.ok(response, 'got a response from Hapi') + assert.ok(body, 'got back a body') + + const errors = agent.errors.traceAggregator.errors + assert.ok(errors, 'errors were found') + assert.equal(errors.length, 1, 'should be 1 error') + + const first = errors[0] + assert.ok(first, 'have the first error') + match(first[2], 'ohno', 'got the expected error') + + end() + } + ) + }) +}) diff --git a/test/versioned/hapi/router.tap.js b/test/versioned/hapi/router.tap.js deleted file mode 100644 index 2870b1d101..0000000000 --- a/test/versioned/hapi/router.tap.js +++ /dev/null @@ -1,374 +0,0 @@ -/* - * Copyright 2020 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 utils = require('./hapi-utils') -const Boom = require('@hapi/boom') - -tap.test('Hapi router introspection', function (t) { - t.autoend() - - let agent = null - let server = null - let port = null - - t.beforeEach(function () { - agent = helper.instrumentMockedAgent({ - attributes: { - enabled: true, - include: ['request.parameters.*'] - } - }) - - server = utils.getServer() - }) - - t.afterEach(function () { - helper.unloadAgent(agent) - return server.stop() - }) - - t.test('using route handler - simple case', function (t) { - agent.on('transactionFinished', verifier(t)) - - server.route({ - method: 'GET', - path: '/test/{id}', - handler: function () { - t.ok(agent.getTransaction(), 'transaction is available') - return { status: 'ok' } - } - }) - - server.start().then(function () { - port = server.info.port - const uri = 'http://localhost:' + port + '/test/31337' - - helper.makeGetRequest(uri, function (_error, res, body) { - t.equal(res.statusCode, 200, 'nothing exploded') - t.same(body, { status: 'ok' }, 'got expected response') - t.end() - }) - }) - }) - - t.test('using route handler under config object', function (t) { - agent.on('transactionFinished', verifier(t)) - - const hello = { - handler: function () { - t.ok(agent.getTransaction(), 'transaction is available') - return { status: 'ok' } - } - } - - server.route({ - method: 'GET', - path: '/test/{id}', - config: hello - }) - - server.start().then(function () { - port = server.info.port - const uri = 'http://localhost:' + port + '/test/31337' - helper.makeGetRequest(uri, function (_error, res, body) { - t.equal(res.statusCode, 200, 'nothing exploded') - t.same(body, { status: 'ok' }, 'got expected response') - t.end() - }) - }) - }) - - t.test('using route handler outside of config object', function (t) { - agent.on('transactionFinished', verifier(t)) - - server.route({ - method: 'GET', - path: '/test/{id}', - config: {}, - handler: function () { - t.ok(agent.getTransaction(), 'transaction is available') - return { status: 'ok' } - } - }) - - server.start().then(function () { - port = server.info.port - const uri = 'http://localhost:' + port + '/test/31337' - helper.makeGetRequest(uri, function (_error, res, body) { - t.equal(res.statusCode, 200, 'nothing exploded') - t.same(body, { status: 'ok' }, 'got expected response') - t.end() - }) - }) - }) - - t.test('using `pre` config option', function (t) { - agent.on('transactionFinished', verifier(t)) - - const route = { - method: 'GET', - path: '/test/{id}', - options: { - pre: [ - function plain() { - t.ok(agent.getTransaction(), 'transaction available in plain `pre` function') - return 'ok' - }, - [ - { - method: function nested() { - t.ok(agent.getTransaction(), 'transaction available in nested `pre` function') - return 'ok' - } - }, - { - assign: 'pre3', - method: function nested2() { - t.ok(agent.getTransaction(), 'transaction available in 2nd nested `pre` function') - return 'ok' - } - } - ] - ], - handler: function (req) { - t.ok(agent.getTransaction(), 'transaction is available in final handler') - return { status: req.pre.pre3 } - } - } - } - server.route(route) - - server.start().then(function () { - port = server.info.port - const uri = 'http://localhost:' + port + '/test/31337' - helper.makeGetRequest(uri, function (_error, res, body) { - t.equal(res.statusCode, 200, 'nothing exploded') - t.same(body, { status: 'ok' }, 'got expected response') - t.end() - }) - }) - }) - - t.test('using custom handler type', function (t) { - agent.on('transactionFinished', verifier(t)) - - server.decorate('handler', 'hello', function () { - return function customHandler() { - t.ok(agent.getTransaction(), 'transaction is available') - return { status: 'ok' } - } - }) - - server.route({ - method: 'GET', - path: '/test/{id}', - handler: { - hello: {} - } - }) - - server.start().then(function () { - port = server.info.port - const uri = 'http://localhost:' + port + '/test/31337' - helper.makeGetRequest(uri, function (_error, res, body) { - t.equal(res.statusCode, 200, 'nothing exploded') - t.same(body, { status: 'ok' }, 'got expected response') - t.end() - }) - }) - }) - - /* - * This test covers the use case of placing defaults on the handler - * function. - * for example: https://github.com/hapijs/h2o2/blob/v6.0.1/lib/index.js#L189-L198 - */ - t.test('using custom handler defaults', function (t) { - agent.on('transactionFinished', verifier(t, 'POST')) - - function handler(route) { - t.equal(route.settings.payload.parse, false, 'should set the payload parse setting') - t.equal(route.settings.payload.output, 'stream', 'should set the payload output setting') - - return function customHandler() { - t.ok(agent.getTransaction(), 'transaction is available') - return { status: 'ok' } - } - } - - handler.defaults = { - payload: { - output: 'stream', - parse: false - } - } - - server.decorate('handler', 'hello', handler) - - server.route({ - method: 'POST', - path: '/test/{id}', - handler: { - hello: {} - } - }) - - server.start().then(function () { - port = server.info.port - const uri = 'http://localhost:' + port + '/test/31337' - helper.makeRequest(uri, { method: 'POST' }, function (_error, res, body) { - t.equal(res.statusCode, 200, 'nothing exploded') - t.same(body, { status: 'ok' }, 'got expected response') - t.end() - }) - }) - }) - - t.test('404 transaction is named correctly', function (t) { - agent.on('transactionFinished', function (tx) { - t.equal( - tx.trace.root.children[0].name, - 'WebTransaction/Nodejs/GET/(not found)', - '404 segment has standardized name' - ) - }) - - server.start().then(function () { - port = server.info.port - const uri = 'http://localhost:' + port + '/test' - helper.makeGetRequest(uri, function (_error, res, body) { - t.equal(res.statusCode, 404, 'nonexistent route was not found') - t.same( - body, - { statusCode: 404, error: 'Not Found', message: 'Not Found' }, - 'got expected response' - ) - t.end() - }) - }) - }) - - t.test('using shared `pre` config option', function (t) { - agent.on('transactionFinished', (transaction) => { - t.equal( - transaction.name, - 'WebTransaction/Hapi/GET//first/{firstId}/second/{secondId}/data', - 'transaction is named correctly' - ) - }) - - // Middlewares if shared across routes causing - // issues with the new relic transactions - const assignStuff = { - method: async ({ params: { firstId, secondId } }) => { - let stuff = null - if (firstId && secondId) { - stuff = await Promise.resolve({ id: 123 }) - } - return stuff || Boom.notFound() - }, - assign: 'stuff' - } - - const assignMoreStuff = { - method: async () => { - return { test: 123 } - }, - assign: 'stuff' - } - - server.route([ - { - method: 'GET', - path: '/first/{firstId}/second/{secondId}/data', // I'm calling this URL - config: { - auth: false, - pre: [assignStuff, assignMoreStuff], - handler: async () => { - return { success: 'TRUE' } - } - } - }, - { - method: 'POST', - path: '/first/{firstId}/second/{secondId}/data', // This one should not be added as well - config: { - auth: false, - pre: [assignStuff], - handler: async () => ({ success: 'TRUE' }) - } - }, - { - method: 'GET', - path: '/first/{firstId}/second/{secondId}/should-not-be-added', - config: { - auth: false, - pre: [assignStuff], - handler: async () => ({ success: 'TRUE' }) - } - }, - { - method: 'GET', - path: '/first/{firstId}/second/{secondId}/should-not-be-added2', - config: { - auth: false, - pre: [assignStuff], - handler: async () => ({ success: 'TRUE' }) - } - }, - { - method: 'GET', - path: '/first/{firstId}/second/{secondId}/should-not-be-added3', - config: { - auth: false, - pre: [assignStuff, assignMoreStuff], - handler: async () => ({ success: 'TRUE' }) - } - } - ]) - - server.start().then(function () { - port = server.info.port - const uri = 'http://localhost:' + port + '/first/123/second/456/data' - helper.makeGetRequest(uri, function (_error, res, body) { - t.equal(res.statusCode, 200, 'nothing exploded') - t.same(body, { success: 'TRUE' }, 'got expected response') - t.end() - }) - }) - }) -}) - -function verifier(t, verb) { - verb = verb || 'GET' - return function (transaction) { - t.equal( - transaction.name, - 'WebTransaction/Hapi/' + verb + '//test/{id}', - 'transaction has expected name' - ) - - t.equal(transaction.url, '/test/31337', 'URL is left alone') - t.equal(transaction.statusCode, 200, 'status code is OK') - t.equal(transaction.verb, verb, 'HTTP method is ' + verb) - t.ok(transaction.trace, 'transaction has trace') - - const web = transaction.trace.root.children[0] - t.ok(web, 'trace has web segment') - t.equal(web.name, transaction.name, 'segment name and transaction name match') - - t.equal(web.partialName, 'Hapi/' + verb + '//test/{id}', 'should have partial name for apdex') - - t.equal( - web.getAttributes()['request.parameters.route.id'], - '31337', - 'namer gets attributes out of route' - ) - } -} diff --git a/test/versioned/hapi/router.test.js b/test/versioned/hapi/router.test.js new file mode 100644 index 0000000000..2e23fcacc9 --- /dev/null +++ b/test/versioned/hapi/router.test.js @@ -0,0 +1,383 @@ +/* + * 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 Boom = require('@hapi/boom') + +const helper = require('../../lib/agent_helper') +const utils = require('./hapi-utils') + +test.beforeEach((ctx) => { + ctx.nr = {} + ctx.nr.agent = helper.instrumentMockedAgent({ + attributes: { + enabled: true, + include: ['request.parameters.*'] + } + }) + + ctx.nr.server = utils.getServer() +}) + +test.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) + ctx.nr.server.stop() +}) + +function verifier(verb = 'GET') { + return function (transaction) { + assert.equal( + transaction.name, + 'WebTransaction/Hapi/' + verb + '//test/{id}', + 'transaction has expected name' + ) + + assert.equal(transaction.url, '/test/31337', 'URL is left alone') + assert.equal(transaction.statusCode, 200, 'status code is OK') + assert.equal(transaction.verb, verb, 'HTTP method is ' + verb) + assert.ok(transaction.trace, 'transaction has trace') + + const web = transaction.trace.root.children[0] + assert.ok(web, 'trace has web segment') + assert.equal(web.name, transaction.name, 'segment name and transaction name match') + + assert.equal( + web.partialName, + 'Hapi/' + verb + '//test/{id}', + 'should have partial name for apdex' + ) + + assert.equal( + web.getAttributes()['request.parameters.route.id'], + '31337', + 'namer gets attributes out of route' + ) + } +} + +test('using route handler - simple case', (t, end) => { + const { agent, server } = t.nr + agent.on('transactionFinished', verifier()) + + server.route({ + method: 'GET', + path: '/test/{id}', + handler: function () { + assert.ok(agent.getTransaction(), 'transaction is available') + return { status: 'ok' } + } + }) + + server.start().then(function () { + const port = server.info.port + const uri = 'http://localhost:' + port + '/test/31337' + + helper.makeGetRequest(uri, {}, function (_error, res, body) { + assert.equal(res.statusCode, 200, 'nothing exploded') + assert.deepStrictEqual(body, { status: 'ok' }, 'got expected response') + end() + }) + }) +}) + +test('using route handler under config object', (t, end) => { + const { agent, server } = t.nr + agent.on('transactionFinished', verifier()) + + const hello = { + handler: function () { + assert.ok(agent.getTransaction(), 'transaction is available') + return { status: 'ok' } + } + } + + server.route({ + method: 'GET', + path: '/test/{id}', + config: hello + }) + + server.start().then(function () { + const port = server.info.port + const uri = 'http://localhost:' + port + '/test/31337' + helper.makeGetRequest(uri, {}, function (_error, res, body) { + assert.equal(res.statusCode, 200, 'nothing exploded') + assert.deepStrictEqual(body, { status: 'ok' }, 'got expected response') + end() + }) + }) +}) + +test('using route handler outside of config object', (t, end) => { + const { agent, server } = t.nr + agent.on('transactionFinished', verifier()) + + server.route({ + method: 'GET', + path: '/test/{id}', + config: {}, + handler: function () { + assert.ok(agent.getTransaction(), 'transaction is available') + return { status: 'ok' } + } + }) + + server.start().then(function () { + const port = server.info.port + const uri = 'http://localhost:' + port + '/test/31337' + helper.makeGetRequest(uri, {}, function (_error, res, body) { + assert.equal(res.statusCode, 200, 'nothing exploded') + assert.deepStrictEqual(body, { status: 'ok' }, 'got expected response') + end() + }) + }) +}) + +test('using `pre` config option', (t, end) => { + const { agent, server } = t.nr + agent.on('transactionFinished', verifier()) + + const route = { + method: 'GET', + path: '/test/{id}', + options: { + pre: [ + function plain() { + assert.ok(agent.getTransaction(), 'transaction available in plain `pre` function') + return 'ok' + }, + [ + { + method: function nested() { + assert.ok(agent.getTransaction(), 'transaction available in nested `pre` function') + return 'ok' + } + }, + { + assign: 'pre3', + method: function nested2() { + assert.ok( + agent.getTransaction(), + 'transaction available in 2nd nested `pre` function' + ) + return 'ok' + } + } + ] + ], + handler: function (req) { + assert.ok(agent.getTransaction(), 'transaction is available in final handler') + return { status: req.pre.pre3 } + } + } + } + server.route(route) + + server.start().then(function () { + const port = server.info.port + const uri = 'http://localhost:' + port + '/test/31337' + helper.makeGetRequest(uri, {}, function (_error, res, body) { + assert.equal(res.statusCode, 200, 'nothing exploded') + assert.deepStrictEqual(body, { status: 'ok' }, 'got expected response') + end() + }) + }) +}) + +test('using custom handler type', (t, end) => { + const { agent, server } = t.nr + agent.on('transactionFinished', verifier()) + + server.decorate('handler', 'hello', function () { + return function customHandler() { + assert.ok(agent.getTransaction(), 'transaction is available') + return { status: 'ok' } + } + }) + + server.route({ + method: 'GET', + path: '/test/{id}', + handler: { + hello: {} + } + }) + + server.start().then(function () { + const port = server.info.port + const uri = 'http://localhost:' + port + '/test/31337' + helper.makeGetRequest(uri, {}, function (_error, res, body) { + assert.equal(res.statusCode, 200, 'nothing exploded') + assert.deepStrictEqual(body, { status: 'ok' }, 'got expected response') + end() + }) + }) +}) + +/* + * This test covers the use case of placing defaults on the handler + * function. + * for example: https://github.com/hapijs/h2o2/blob/v6.0.1/lib/index.js#L189-L198 + */ +test('using custom handler defaults', (t, end) => { + const { agent, server } = t.nr + agent.on('transactionFinished', verifier('POST')) + + function handler(route) { + assert.equal(route.settings.payload.parse, false, 'should set the payload parse setting') + assert.equal(route.settings.payload.output, 'stream', 'should set the payload output setting') + + return function customHandler() { + assert.ok(agent.getTransaction(), 'transaction is available') + return { status: 'ok' } + } + } + + handler.defaults = { + payload: { + output: 'stream', + parse: false + } + } + + server.decorate('handler', 'hello', handler) + + server.route({ + method: 'POST', + path: '/test/{id}', + handler: { + hello: {} + } + }) + + server.start().then(function () { + const port = server.info.port + const uri = 'http://localhost:' + port + '/test/31337' + helper.makeRequest(uri, { method: 'POST' }, function (_error, res, body) { + assert.equal(res.statusCode, 200, 'nothing exploded') + assert.deepStrictEqual(body, { status: 'ok' }, 'got expected response') + end() + }) + }) +}) + +test('404 transaction is named correctly', (t, end) => { + const { agent, server } = t.nr + agent.on('transactionFinished', function (tx) { + assert.equal( + tx.trace.root.children[0].name, + 'WebTransaction/Nodejs/GET/(not found)', + '404 segment has standardized name' + ) + }) + + server.start().then(function () { + const port = server.info.port + const uri = 'http://localhost:' + port + '/test' + helper.makeGetRequest(uri, {}, function (_error, res, body) { + assert.equal(res.statusCode, 404, 'nonexistent route was not found') + assert.deepStrictEqual( + body, + { statusCode: 404, error: 'Not Found', message: 'Not Found' }, + 'got expected response' + ) + end() + }) + }) +}) + +test('using shared `pre` config option', (t, end) => { + const { agent, server } = t.nr + agent.on('transactionFinished', (transaction) => { + assert.equal( + transaction.name, + 'WebTransaction/Hapi/GET//first/{firstId}/second/{secondId}/data', + 'transaction is named correctly' + ) + }) + + // Middlewares if shared across routes causing + // issues with the new relic transactions + const assignStuff = { + method: async ({ params: { firstId, secondId } }) => { + let stuff = null + if (firstId && secondId) { + stuff = await Promise.resolve({ id: 123 }) + } + return stuff || Boom.notFound() + }, + assign: 'stuff' + } + + const assignMoreStuff = { + method: async () => { + return { test: 123 } + }, + assign: 'stuff' + } + + server.route([ + { + method: 'GET', + path: '/first/{firstId}/second/{secondId}/data', // I'm calling this URL + config: { + auth: false, + pre: [assignStuff, assignMoreStuff], + handler: async () => { + return { success: 'TRUE' } + } + } + }, + { + method: 'POST', + path: '/first/{firstId}/second/{secondId}/data', // This one should not be added as well + config: { + auth: false, + pre: [assignStuff], + handler: async () => ({ success: 'TRUE' }) + } + }, + { + method: 'GET', + path: '/first/{firstId}/second/{secondId}/should-not-be-added', + config: { + auth: false, + pre: [assignStuff], + handler: async () => ({ success: 'TRUE' }) + } + }, + { + method: 'GET', + path: '/first/{firstId}/second/{secondId}/should-not-be-added2', + config: { + auth: false, + pre: [assignStuff], + handler: async () => ({ success: 'TRUE' }) + } + }, + { + method: 'GET', + path: '/first/{firstId}/second/{secondId}/should-not-be-added3', + config: { + auth: false, + pre: [assignStuff, assignMoreStuff], + handler: async () => ({ success: 'TRUE' }) + } + } + ]) + + server.start().then(function () { + const port = server.info.port + const uri = 'http://localhost:' + port + '/first/123/second/456/data' + helper.makeGetRequest(uri, {}, function (_error, res, body) { + assert.equal(res.statusCode, 200, 'nothing exploded') + assert.deepStrictEqual(body, { success: 'TRUE' }, 'got expected response') + end() + }) + }) +}) diff --git a/test/versioned/hapi/segments.tap.js b/test/versioned/hapi/segments.tap.js deleted file mode 100644 index 90d745f893..0000000000 --- a/test/versioned/hapi/segments.tap.js +++ /dev/null @@ -1,288 +0,0 @@ -/* - * Copyright 2020 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 http = require('http') -require('../../lib/metrics_helper') -const NAMES = require('../../../lib/metrics/names') -const utils = require('./hapi-utils') - -let agent -let server -let port - -tap.test('Hapi segments', function (t) { - t.autoend() - - t.beforeEach(function () { - agent = helper.instrumentMockedAgent() - - server = utils.getServer() - }) - - t.afterEach(function () { - helper.unloadAgent(agent) - return server.stop() - }) - - t.test('route handler is recorded as middleware', function (t) { - server.route({ - method: 'GET', - path: '/test', - handler: function myHandler() { - return 'ok' - } - }) - - runTest(t, function (segments, transaction) { - checkMetrics(t, transaction.metrics, [NAMES.HAPI.MIDDLEWARE + 'myHandler//test']) - t.assertSegments(transaction.trace.root.children[0], [ - NAMES.HAPI.MIDDLEWARE + 'myHandler//test' - ]) - t.end() - }) - }) - - t.test('custom handler type is recorded as middleware', function (t) { - server.decorate('handler', 'customHandler', function (route, options) { - return function customHandler() { - return options.key1 - } - }) - - server.route({ - method: 'GET', - path: '/test', - handler: { customHandler: { key1: 'val1' } } - }) - - runTest(t, function (segments, transaction) { - checkMetrics(t, transaction.metrics, [NAMES.HAPI.MIDDLEWARE + 'customHandler//test']) - t.assertSegments(transaction.trace.root.children[0], [ - NAMES.HAPI.MIDDLEWARE + 'customHandler//test' - ]) - t.end() - }) - }) - - t.test('extensions are recorded as middleware', function (t) { - server.ext('onRequest', function (req, h) { - return h.continue - }) - - server.route({ - method: 'GET', - path: '/test', - handler: function myHandler() { - return 'ok' - } - }) - - runTest(t, function (segments, transaction) { - checkMetrics(t, transaction.metrics, [ - NAMES.HAPI.MIDDLEWARE + '//onRequest', - NAMES.HAPI.MIDDLEWARE + 'myHandler//test' - ]) - t.assertSegments(transaction.trace.root.children[0], [ - NAMES.HAPI.MIDDLEWARE + '//onRequest', - NAMES.HAPI.MIDDLEWARE + 'myHandler//test' - ]) - t.end() - }) - }) - - t.test('custom route handler and extension recorded as middleware', function (t) { - server.ext('onRequest', function (req, h) { - return h.continue - }) - - server.decorate('handler', 'customHandler', function (route, options) { - return function customHandler() { - return options.key1 - } - }) - - server.route({ - method: 'GET', - path: '/test', - handler: { customHandler: { key1: 'val1' } } - }) - - runTest(t, function (segments, transaction) { - checkMetrics(t, transaction.metrics, [ - NAMES.HAPI.MIDDLEWARE + '//onRequest', - NAMES.HAPI.MIDDLEWARE + 'customHandler//test' - ]) - t.assertSegments(transaction.trace.root.children[0], [ - NAMES.HAPI.MIDDLEWARE + '//onRequest', - NAMES.HAPI.MIDDLEWARE + 'customHandler//test' - ]) - t.end() - }) - }) - - const filepath = 'test/versioned/hapi/segments.tap.js' - - ;[true, false].forEach((clmEnabled) => { - t.test( - `should ${ - clmEnabled ? 'add' : 'not add' - } CLM attribute to extension function and handler function segments when CLM is ${ - clmEnabled ? 'enabled' : 'disabled' - }`, - (t) => { - agent.config.code_level_metrics.enabled = clmEnabled - server.ext('onRequest', function requestExtension(req, h) { - return h.continue - }) - - server.route({ - method: 'GET', - path: '/test', - handler: function myHandler() { - return 'ok' - } - }) - - runTest(t, function (segments) { - const [onRequestSegment, handlerSegment] = segments - t.clmAttrs({ - segments: [ - { - segment: onRequestSegment, - name: 'requestExtension', - filepath - }, - { - segment: handlerSegment, - name: 'myHandler', - filepath - } - ], - enabled: clmEnabled - }) - t.end() - }) - } - ) - - t.test( - `should ${ - clmEnabled ? 'add' : 'not add' - } CLM attribute to custom handler segments when CLM is ${ - clmEnabled ? 'enabled' : 'disabled' - }`, - (t) => { - agent.config.code_level_metrics.enabled = clmEnabled - server.decorate('handler', 'customHandler', function (route, options) { - return function customHandler() { - return options.key1 - } - }) - - server.route({ - method: 'GET', - path: '/test', - handler: { customHandler: { key1: 'val1' } } - }) - - runTest(t, function ([customHandlerSegment]) { - t.clmAttrs({ - segments: [ - { - segment: customHandlerSegment, - name: 'customHandler', - filepath - } - ], - enabled: clmEnabled - }) - t.end() - }) - } - ) - - t.test( - `should ${ - clmEnabled ? 'add' : 'not add' - } CLM attribute to plugin handler segments when CLM is ${ - clmEnabled ? 'enabled' : 'disabled' - }`, - (t) => { - agent.config.code_level_metrics.enabled = clmEnabled - const plugin = { - register: function (srvr) { - srvr.route({ - method: 'GET', - path: '/test', - handler: function pluginHandler() { - return Promise.resolve('hello') - } - }) - }, - name: 'foobar' - } - - server.register(plugin).then(() => { - runTest(t, function ([pluginHandlerSegment]) { - t.clmAttrs({ - segments: [ - { - segment: pluginHandlerSegment, - name: 'pluginHandler', - filepath - } - ], - enabled: clmEnabled - }) - t.end() - }) - }) - } - ) - }) -}) - -function runTest(t, callback) { - agent.on('transactionFinished', function (tx) { - const baseSegment = tx.trace.root.children[0] - callback(baseSegment.children, tx) - }) - - server.start().then(function () { - port = server.info.port - http - .request({ port: port, path: '/test' }, function (response) { - response.resume() - }) - .end() - }) -} - -function checkMetrics(t, metrics, expected, path) { - path = path || '/test' - const expectedAll = [ - [{ name: 'WebTransaction' }], - [{ name: 'WebTransactionTotalTime' }], - [{ name: 'HttpDispatcher' }], - [{ name: 'WebTransaction/Hapi/GET/' + path }], - [{ name: 'WebTransactionTotalTime/Hapi/GET/' + path }], - [{ name: 'DurationByCaller/Unknown/Unknown/Unknown/Unknown/all' }], - [{ name: 'DurationByCaller/Unknown/Unknown/Unknown/Unknown/allWeb' }], - [{ name: 'Apdex/Hapi/GET/' + path }], - [{ name: 'Apdex' }] - ] - - for (let i = 0; i < expected.length; i++) { - const metric = expected[i] - expectedAll.push([{ name: metric }]) - expectedAll.push([{ name: metric, scope: 'WebTransaction/Hapi/GET/' + path }]) - } - - t.assertMetrics(metrics, expectedAll, true, false) -} diff --git a/test/versioned/hapi/segments.test.js b/test/versioned/hapi/segments.test.js new file mode 100644 index 0000000000..e16468491e --- /dev/null +++ b/test/versioned/hapi/segments.test.js @@ -0,0 +1,287 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const test = require('node:test') +const http = require('node:http') + +const helper = require('../../lib/agent_helper') +const assertClmAttrs = require('../../lib/custom-assertions/assert-clm-attrs') +const assertMetrics = require('../../lib/custom-assertions/assert-metrics') +const assertSegments = require('../../lib/custom-assertions/assert-segments') +const utils = require('./hapi-utils') + +const NAMES = require('../../../lib/metrics/names') + +test.beforeEach((ctx) => { + ctx.nr = {} + ctx.nr.agent = helper.instrumentMockedAgent() + + ctx.nr.server = utils.getServer() +}) + +test.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) + ctx.nr.server.stop() +}) + +function runTest(agent, server, callback) { + agent.on('transactionFinished', function (tx) { + const baseSegment = tx.trace.root.children[0] + callback(baseSegment.children, tx) + }) + + server.start().then(function () { + const port = server.info.port + http + .request({ port: port, path: '/test' }, function (response) { + response.resume() + }) + .end() + }) +} + +function checkMetrics(metrics, expected, path) { + path = path || '/test' + const expectedAll = [ + [{ name: 'WebTransaction' }], + [{ name: 'WebTransactionTotalTime' }], + [{ name: 'HttpDispatcher' }], + [{ name: 'WebTransaction/Hapi/GET/' + path }], + [{ name: 'WebTransactionTotalTime/Hapi/GET/' + path }], + [{ name: 'DurationByCaller/Unknown/Unknown/Unknown/Unknown/all' }], + [{ name: 'DurationByCaller/Unknown/Unknown/Unknown/Unknown/allWeb' }], + [{ name: 'Apdex/Hapi/GET/' + path }], + [{ name: 'Apdex' }] + ] + + for (let i = 0; i < expected.length; i++) { + const metric = expected[i] + expectedAll.push([{ name: metric }]) + expectedAll.push([{ name: metric, scope: 'WebTransaction/Hapi/GET/' + path }]) + } + + assertMetrics(metrics, expectedAll, true, false) +} + +test('route handler is recorded as middleware', (t, end) => { + const { agent, server } = t.nr + + server.route({ + method: 'GET', + path: '/test', + handler: function myHandler() { + return 'ok' + } + }) + + runTest(agent, server, function (segments, transaction) { + checkMetrics(transaction.metrics, [NAMES.HAPI.MIDDLEWARE + 'myHandler//test']) + assertSegments(transaction.trace.root.children[0], [NAMES.HAPI.MIDDLEWARE + 'myHandler//test']) + end() + }) +}) + +test('custom handler type is recorded as middleware', (t, end) => { + const { agent, server } = t.nr + + server.decorate('handler', 'customHandler', function (route, options) { + return function customHandler() { + return options.key1 + } + }) + + server.route({ + method: 'GET', + path: '/test', + handler: { customHandler: { key1: 'val1' } } + }) + + runTest(agent, server, function (segments, transaction) { + checkMetrics(transaction.metrics, [NAMES.HAPI.MIDDLEWARE + 'customHandler//test']) + assertSegments(transaction.trace.root.children[0], [ + NAMES.HAPI.MIDDLEWARE + 'customHandler//test' + ]) + end() + }) +}) + +test('extensions are recorded as middleware', (t, end) => { + const { agent, server } = t.nr + + server.ext('onRequest', function (req, h) { + return h.continue + }) + + server.route({ + method: 'GET', + path: '/test', + handler: function myHandler() { + return 'ok' + } + }) + + runTest(agent, server, function (segments, transaction) { + checkMetrics(transaction.metrics, [ + NAMES.HAPI.MIDDLEWARE + '//onRequest', + NAMES.HAPI.MIDDLEWARE + 'myHandler//test' + ]) + assertSegments(transaction.trace.root.children[0], [ + NAMES.HAPI.MIDDLEWARE + '//onRequest', + NAMES.HAPI.MIDDLEWARE + 'myHandler//test' + ]) + end() + }) +}) + +test('custom route handler and extension recorded as middleware', (t, end) => { + const { agent, server } = t.nr + + server.ext('onRequest', function (req, h) { + return h.continue + }) + + server.decorate('handler', 'customHandler', function (route, options) { + return function customHandler() { + return options.key1 + } + }) + + server.route({ + method: 'GET', + path: '/test', + handler: { customHandler: { key1: 'val1' } } + }) + + runTest(agent, server, function (segments, transaction) { + checkMetrics(transaction.metrics, [ + NAMES.HAPI.MIDDLEWARE + '//onRequest', + NAMES.HAPI.MIDDLEWARE + 'customHandler//test' + ]) + assertSegments(transaction.trace.root.children[0], [ + NAMES.HAPI.MIDDLEWARE + '//onRequest', + NAMES.HAPI.MIDDLEWARE + 'customHandler//test' + ]) + end() + }) +}) + +const filepath = 'test/versioned/hapi/segments.test.js' +for (const clmEnabled of [true, false]) { + test(`should ${ + clmEnabled ? 'add' : 'not add' + } CLM attribute to extension function and handler function segments when CLM is ${ + clmEnabled ? 'enabled' : 'disabled' + }`, (t, end) => { + const { agent, server } = t.nr + + agent.config.code_level_metrics.enabled = clmEnabled + server.ext('onRequest', function requestExtension(req, h) { + return h.continue + }) + + server.route({ + method: 'GET', + path: '/test', + handler: function myHandler() { + return 'ok' + } + }) + + runTest(agent, server, function (segments) { + const [onRequestSegment, handlerSegment] = segments + assertClmAttrs({ + segments: [ + { + segment: onRequestSegment, + name: 'requestExtension', + filepath + }, + { + segment: handlerSegment, + name: 'myHandler', + filepath + } + ], + enabled: clmEnabled + }) + end() + }) + }) + + test(`should ${ + clmEnabled ? 'add' : 'not add' + } CLM attribute to custom handler segments when CLM is ${ + clmEnabled ? 'enabled' : 'disabled' + }`, (t, end) => { + const { agent, server } = t.nr + + agent.config.code_level_metrics.enabled = clmEnabled + server.decorate('handler', 'customHandler', function (route, options) { + return function customHandler() { + return options.key1 + } + }) + + server.route({ + method: 'GET', + path: '/test', + handler: { customHandler: { key1: 'val1' } } + }) + + runTest(agent, server, function ([customHandlerSegment]) { + assertClmAttrs({ + segments: [ + { + segment: customHandlerSegment, + name: 'customHandler', + filepath + } + ], + enabled: clmEnabled + }) + end() + }) + }) + + test(`should ${ + clmEnabled ? 'add' : 'not add' + } CLM attribute to plugin handler segments when CLM is ${ + clmEnabled ? 'enabled' : 'disabled' + }`, (t, end) => { + const { agent, server } = t.nr + + agent.config.code_level_metrics.enabled = clmEnabled + const plugin = { + register: function (srvr) { + srvr.route({ + method: 'GET', + path: '/test', + handler: function pluginHandler() { + return Promise.resolve('hello') + } + }) + }, + name: 'foobar' + } + + server.register(plugin).then(() => { + runTest(agent, server, function ([pluginHandlerSegment]) { + assertClmAttrs({ + segments: [ + { + segment: pluginHandlerSegment, + name: 'pluginHandler', + filepath + } + ], + enabled: clmEnabled + }) + end() + }) + }) + }) +} diff --git a/test/versioned/hapi/vhost.tap.js b/test/versioned/hapi/vhost.tap.js deleted file mode 100644 index 12536c01a8..0000000000 --- a/test/versioned/hapi/vhost.tap.js +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const { DESTINATIONS } = require('../../../lib/config/attribute-filter') -const tap = require('tap') -const helper = require('../../lib/agent_helper') -const utils = require('./hapi-utils') -const HTTP_ATTS = require('../../lib/fixtures').httpAttributes - -tap.test('Hapi vhost support', function (t) { - t.autoend() - - t.test('should not explode when using vhosts', function (t) { - const agent = helper.instrumentMockedAgent({ - attributes: { - enabled: true, - include: ['request.parameters.*'] - } - }) - - const server = utils.getServer() - let port - - t.teardown(function () { - return server.stop() - }) - - agent.on('transactionFinished', function (tx) { - t.ok(tx.trace, 'transaction has a trace.') - const attributes = tx.trace.attributes.get(DESTINATIONS.TRANS_TRACE) - HTTP_ATTS.forEach(function (key) { - t.ok(attributes[key], 'Trace contains expected HTTP attribute: ' + key) - }) - t.equal( - attributes['request.parameters.route.id'], - '1337', - 'Trace attributes include `id` route param' - ) - t.equal( - attributes['request.parameters.name'], - 'hapi', - 'Trace attributes include `name` query param' - ) - - helper.unloadAgent(agent) - }) - - server.route({ - method: 'GET', - path: '/test/{id}/', - vhost: 'localhost', - handler: function () { - t.ok(agent.getTransaction(), 'transaction is available') - return { status: 'ok' } - } - }) - - server.route({ - method: 'GET', - path: '/test/{id}/2', - vhost: 'localhost', - handler: function () { - t.ok(agent.getTransaction(), 'transaction is available') - return { status: 'ok' } - } - }) - - server.start().then(function () { - port = server.info.port - const uri = 'http://localhost:' + port + '/test/1337/2?name=hapi' - helper.makeRequest(uri, function (_error, res, body) { - t.equal(res.statusCode, 200, 'nothing exploded') - t.same(body, { status: 'ok' }, 'got expected response') - t.end() - }) - }) - }) -}) diff --git a/test/versioned/hapi/vhost.test.js b/test/versioned/hapi/vhost.test.js new file mode 100644 index 0000000000..9f1e9d4a24 --- /dev/null +++ b/test/versioned/hapi/vhost.test.js @@ -0,0 +1,86 @@ +/* + * 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 utils = require('./hapi-utils') + +const { DESTINATIONS } = require('../../../lib/config/attribute-filter') +const HTTP_ATTS = require('../../lib/fixtures').httpAttributes + +test.beforeEach((ctx) => { + ctx.nr = {} + ctx.nr.agent = helper.instrumentMockedAgent({ + attributes: { + enabled: true, + include: ['request.parameters.*'] + } + }) + + ctx.nr.server = utils.getServer() +}) + +test.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) + ctx.nr.server.stop() +}) + +test('should not explode when using vhosts', (t, end) => { + const { agent, server } = t.nr + + agent.on('transactionFinished', (tx) => { + assert.ok(tx.trace, 'transaction has a trace.') + const attributes = tx.trace.attributes.get(DESTINATIONS.TRANS_TRACE) + HTTP_ATTS.forEach(function (key) { + assert.ok(attributes[key], 'Trace contains expected HTTP attribute: ' + key) + }) + assert.equal( + attributes['request.parameters.route.id'], + '1337', + 'Trace attributes include `id` route param' + ) + assert.equal( + attributes['request.parameters.name'], + 'hapi', + 'Trace attributes include `name` query param' + ) + + helper.unloadAgent(agent) + }) + + server.route({ + method: 'GET', + path: '/test/{id}/', + vhost: 'localhost', + handler: function () { + assert.ok(agent.getTransaction(), 'transaction is available') + return { status: 'ok' } + } + }) + + server.route({ + method: 'GET', + path: '/test/{id}/2', + vhost: 'localhost', + handler: function () { + assert.ok(agent.getTransaction(), 'transaction is available') + return { status: 'ok' } + } + }) + + server.start().then(function () { + const port = server.info.port + const uri = 'http://localhost:' + port + '/test/1337/2?name=hapi' + helper.makeRequest(uri, {}, function (_error, res, body) { + assert.equal(res.statusCode, 200, 'nothing exploded') + assert.deepStrictEqual(body, { status: 'ok' }, 'got expected response') + end() + }) + }) +})