diff --git a/lib/collector/api.js b/lib/collector/api.js index e98e8ead00..f9e7ced2eb 100644 --- a/lib/collector/api.js +++ b/lib/collector/api.js @@ -30,9 +30,29 @@ const BACKOFFS = [ // Expected collector response codes const SUCCESS = new Set([200, 202]) -const RESTART = new Set([401, 409]) -const FAILURE_SAVE_DATA = new Set([408, 429, 500, 503]) -const FAILURE_DISCARD_DATA = new Set([400, 403, 404, 405, 407, 411, 413, 414, 415, 417, 431]) +const RESTART = new Set([ + 401, // Authentication failed. + 409 // NR says to reconnect for some reason. +]) +const FAILURE_SAVE_DATA = new Set([ + 408, // Data took too long to reach NR. + 429, // Too many requests being received by NR, rate limited. + 500, // NR server went boom. + 503 // NR server is not available. +]) +const FAILURE_DISCARD_DATA = new Set([ + 400, // Format of the request is incorrect. + 403, // Not entitled to perform the action. + 404, // Sending to wrong destination. + 405, // Using the wrong HTTP method (e.g. PUT instead of POST). + 407, // Proxy authentication misconfigured. + 411, // No Content-Length header provided, or value is incorrect. + 413, // Payload is too large. + 414, // URI exceeds allowed length. + 415, // Content-type or Content-encoding values are incorrect. + 417, // NR cannot meet the expectation of the request. + 431 // Request headers exceed size limit. +]) const AGENT_RUN_BEHAVIOR = CollectorResponse.AGENT_RUN_BEHAVIOR diff --git a/test/lib/assert-metrics.js b/test/lib/assert-metrics.js new file mode 100644 index 0000000000..261db99fbc --- /dev/null +++ b/test/lib/assert-metrics.js @@ -0,0 +1,57 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +module.exports = { + assertMetricValues +} + +const assert = require('node:assert') + +/** + * @param {Transaction} transaction Nodejs agent transaction + * @param {Array} expected Array of metric data where metric data is in this form: + * [ + * { + * “name”:”name of metric”, + * “scope”:”scope of metric”, + * }, + * [count, + * total time, + * exclusive time, + * min time, + * max time, + * sum of squares] + * ] + * @param {boolean} exact When true, found and expected metric lengths should match + */ +function assertMetricValues(transaction, expected, exact) { + const metrics = transaction.metrics + + for (let i = 0; i < expected.length; ++i) { + let expectedMetric = Object.assign({}, expected[i]) + let name = null + let scope = null + + if (typeof expectedMetric === 'string') { + name = expectedMetric + expectedMetric = {} + } else { + name = expectedMetric[0].name + scope = expectedMetric[0].scope + } + + const metric = metrics.getMetric(name, scope) + assert.ok(metric, 'should have expected metric name') + + assert.deepStrictEqual(metric.toJSON(), expectedMetric[1], 'metric values should match') + } + + if (exact) { + const metricsJSON = metrics.toJSON() + assert.equal(metricsJSON.length, expected.length, 'metrics length should match') + } +} diff --git a/test/lib/test-collector.js b/test/lib/test-collector.js index 797fa5bf66..20e385581f 100644 --- a/test/lib/test-collector.js +++ b/test/lib/test-collector.js @@ -18,8 +18,10 @@ class Collector { #handlers = new Map() #server #address + #runId - constructor() { + constructor({ runId = 42 } = {}) { + this.#runId = runId this.#server = https.createServer({ key: fakeCert.privateKey, cert: fakeCert.certificate @@ -37,6 +39,27 @@ class Collector { this.end(JSON.stringify(payload)) } + req.body = function () { + let resolve + let reject + const promise = new Promise((res, rej) => { + resolve = res + reject = rej + }) + + let data = '' + this.on('data', (d) => { + data += d + }) + this.on('end', () => { + resolve(data) + }) + this.on('error', (error) => { + reject(error) + }) + return promise + } + handler.isDone = true handler(req, res) }) @@ -104,6 +127,19 @@ class Collector { } } + /** + * the most basic `connect` handler. Useful when you do not need to + * customize the handler. + * + * @returns {function} + */ + get connectHandler() { + const runId = this.#runId + return function (req, res) { + res.json({ payload: { return_value: { agent_run_id: runId } } }) + } + } + /** * The most basic `preconnect` handler. Useful when you do not need to * customize the handler. @@ -135,7 +171,9 @@ class Collector { * requests. * @param {function} handler A typical `(req, res) => {}` handler. For * convenience, `res` is extended with a `json({ payload, code = 200 })` - * method for easily sending JSON responses. + * method for easily sending JSON responses. Also, `req` is extended with + * a `body()` method that returns a promise which resolves to the string + * data supplied via POST-like requests. */ addHandler(endpoint, handler) { const qs = querystring.decode(endpoint.slice(endpoint.indexOf('?') + 1)) @@ -181,11 +219,12 @@ class Collector { // Add handlers for the required agent startup connections. These should // be overwritten by tests that exercise the startup phase, but adding these // stubs makes it easier to test other connection events. - this.addHandler(helper.generateCollectorPath('preconnect', 42), this.preconnectHandler) - this.addHandler(helper.generateCollectorPath('connect', 42), (req, res) => { - res.json({ payload: { return_value: { agent_run_id: 42 } } }) - }) - this.addHandler(helper.generateCollectorPath('agent_settings', 42), this.agentSettingsHandler) + this.addHandler(helper.generateCollectorPath('preconnect', this.#runId), this.preconnectHandler) + this.addHandler(helper.generateCollectorPath('connect', this.#runId), this.connectHandler) + this.addHandler( + helper.generateCollectorPath('agent_settings', this.#runId), + this.agentSettingsHandler + ) return address } diff --git a/test/unit/collector/api-connect.test.js b/test/unit/collector/api-connect.test.js index d53e6464d3..fc80b68930 100644 --- a/test/unit/collector/api-connect.test.js +++ b/test/unit/collector/api-connect.test.js @@ -1,343 +1,227 @@ /* - * 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 tap = require('tap') +const test = require('node:test') +const assert = require('node:assert') const nock = require('nock') -const sinon = require('sinon') const proxyquire = require('proxyquire') +const Collector = require('../../lib/test-collector') +const CollectorResponse = require('../../../lib/collector/response') const helper = require('../../lib/agent_helper') +const { securityPolicies } = require('../../lib/fixtures') const CollectorApi = require('../../../lib/collector/api') -const CollectorResponse = require('../../../lib/collector/response') -const securityPolicies = require('../../lib/fixtures').securityPolicies -const HOST = 'collector.newrelic.com' -const REDIRECT_HOST = 'unique.newrelic.com' -const PORT = 443 -const URL = 'https://' + HOST -const CONNECT_URL = `https://${REDIRECT_HOST}` const RUN_ID = 1337 +const baseAgentConfig = { + app_name: ['TEST'], + ssl: true, + license_key: 'license key here', + utilization: { + detect_aws: false, + detect_pcf: false, + detect_azure: false, + detect_gcp: false, + detect_docker: false + }, + browser_monitoring: {}, + transaction_tracer: {} +} -const timeout = global.setTimeout - -tap.test('requires a callback', (t) => { - const agent = setupMockedAgent() - const collectorApi = new CollectorApi(agent) - - t.teardown(() => { +test('requires a callback', (t) => { + const agent = helper.loadMockedAgent(baseAgentConfig) + agent.reconfigure = () => {} + agent.setState = () => {} + t.after(() => { helper.unloadAgent(agent) }) - t.throws(() => { - collectorApi.connect(null) - }, 'callback is required') - - t.end() + const collectorApi = new CollectorApi(agent) + assert.throws( + () => { + collectorApi.connect(null) + }, + { message: 'callback is required' } + ) }) -tap.test('receiving 200 response, with valid data', (t) => { - t.autoend() - - let agent = null - let collectorApi = null - - let redirection = null - let connection = null - - const validSsc = { - agent_run_id: RUN_ID - } - - t.beforeEach(() => { - agent = setupMockedAgent() - collectorApi = new CollectorApi(agent) - - nock.disableNetConnect() - - const response = { return_value: validSsc } - - redirection = nock(URL + ':443') - .post(helper.generateCollectorPath('preconnect')) - .reply(200, { return_value: { redirect_host: REDIRECT_HOST, security_policies: {} } }) - - connection = nock(CONNECT_URL) - .post(helper.generateCollectorPath('connect')) - .reply(200, response) - }) - - t.afterEach(() => { - if (!nock.isDone()) { - /* eslint-disable no-console */ - console.error('Cleaning pending mocks: %j', nock.pendingMocks()) - /* eslint-enable no-console */ - nock.cleanAll() - } - - nock.enableNetConnect() - - helper.unloadAgent(agent) - agent = null - collectorApi = null - }) +test('receiving 200 response, with valid data', async (t) => { + t.beforeEach(beforeEach) + t.afterEach(afterEach) - t.test('should not error out', (t) => { + await t.test('should not error out', (t, end) => { + const { collectorApi } = t.nr collectorApi.connect((error) => { - t.error(error) - - redirection.done() - connection.done() - - t.end() + assert.equal(error, undefined) + end() }) }) - t.test('should pass through server-side configuration untouched', (t) => { + await t.test('should pass through server-side configuration untouched', (t, end) => { + const { collectorApi } = t.nr collectorApi.connect((error, res) => { - const ssc = res.payload - t.same(ssc, validSsc) - - redirection.done() - connection.done() - - t.end() + assert.equal(error, undefined) + assert.deepStrictEqual(res.payload, { agent_run_id: RUN_ID }) + end() }) }) }) -tap.test('succeeds when given a different port number for redirect', (t) => { - t.autoend() - - let agent = null - let collectorApi = null - - let redirection = null - let connection = null +test('succeeds when given a different port number for redirect', async (t) => { + t.beforeEach(beforeEach) + t.afterEach(afterEach) - const validSsc = { - agent_run_id: RUN_ID - } - - t.beforeEach(() => { - agent = setupMockedAgent() - collectorApi = new CollectorApi(agent) - - nock.disableNetConnect() - - const response = { return_value: validSsc } - - redirection = nock(URL + ':443') - .post(helper.generateCollectorPath('preconnect')) - .reply(200, { - return_value: { redirect_host: REDIRECT_HOST + ':8089', security_policies: {} } - }) - - connection = nock(CONNECT_URL + ':8089') - .post(helper.generateCollectorPath('connect')) - .reply(200, response) - }) - - t.afterEach(() => { - if (!nock.isDone()) { - /* eslint-disable no-console */ - console.error('Cleaning pending mocks: %j', nock.pendingMocks()) - /* eslint-enable no-console */ - nock.cleanAll() - } - - nock.enableNetConnect() - - helper.unloadAgent(agent) - agent = null - collectorApi = null - }) - - t.test('should not error out', (t) => { + await t.test('should not error out', (t, end) => { + const { collectorApi } = t.nr collectorApi.connect((error) => { - t.error(error) - - t.end() + assert.equal(error, undefined) + end() }) }) - t.test('should have the correct hostname', (t) => { + await t.test('should have the correct hostname', (t, end) => { + const { collector, collectorApi } = t.nr collectorApi.connect(() => { const methods = collectorApi._methods Object.keys(methods) - .filter((key) => { - return key !== 'preconnect' - }) + .filter((key) => key !== 'preconnect') .forEach((key) => { - t.equal(methods[key].endpoint.host, REDIRECT_HOST) + assert.equal(methods[key].endpoint.host, collector.host) }) - - t.end() + end() }) }) - t.test('should not change config host', (t) => { + await t.test('should not change config host', (t, end) => { + const { collector, collectorApi } = t.nr collectorApi.connect(() => { - t.equal(collectorApi._agent.config.host, HOST) - - t.end() + assert.equal(collectorApi._agent.config.host, collector.host) + end() }) }) - t.test('should update endpoints with correct port number', (t) => { + await t.test('should update endpoints with correct port number', (t, end) => { + const { collector, collectorApi } = t.nr collectorApi.connect(() => { const methods = collectorApi._methods Object.keys(methods) - .filter((key) => { - return key !== 'preconnect' - }) + .filter((key) => key !== 'preconnect') .forEach((key) => { - t.equal(methods[key].endpoint.port, '8089') + assert.equal(methods[key].endpoint.port, collector.port) }) - - t.end() + end() }) }) - t.test('should not update preconnect endpoint', (t) => { + await t.test('should not update preconnect endpoint', (t, end) => { + const { collector, collectorApi } = t.nr collectorApi.connect(() => { - t.equal(collectorApi._methods.preconnect.endpoint.host, HOST) - t.equal(collectorApi._methods.preconnect.endpoint.port, 443) - - t.end() + assert.equal(collectorApi._methods.preconnect.endpoint.host, collector.host) + assert.equal(collectorApi._methods.preconnect.endpoint.port, collector.port) + end() }) }) - t.test('should not change config port number', (t) => { + await t.test('should not change config port number', (t, end) => { + const { collector, collectorApi } = t.nr collectorApi.connect(() => { - t.equal(collectorApi._agent.config.port, 443) - - t.end() + assert.equal(collectorApi._agent.config.port, collector.port) + end() }) }) - t.test('should have a run ID', (t) => { - collectorApi.connect(function test(error, res) { - const ssc = res.payload - t.equal(ssc.agent_run_id, RUN_ID) - - redirection.done() - connection.done() - - t.end() + await t.test('should have a run ID', (t, end) => { + const { collectorApi } = t.nr + collectorApi.connect((error, res) => { + assert.equal(res.payload.agent_run_id, RUN_ID) + end() }) }) - t.test('should pass through server-side configuration untouched', (t) => { - collectorApi.connect(function test(error, res) { - const ssc = res.payload - t.same(ssc, validSsc) - - redirection.done() - connection.done() - - t.end() + await t.test('should pass through server-side configuration untouched', (t, end) => { + const { collectorApi } = t.nr + collectorApi.connect((error, res) => { + assert.deepStrictEqual(res.payload, { agent_run_id: RUN_ID }) + end() }) }) }) -const retryCount = [1, 5] - -retryCount.forEach((count) => { - tap.test(`succeeds after ${count} 503s on preconnect`, (t) => { - t.autoend() - - let collectorApi = null - let agent = null - - const valid = { - agent_run_id: RUN_ID - } - - const response = { return_value: valid } - - let failure = null - let success = null - let connection = null - - let bad = null - let ssc = null - - t.beforeEach(() => { - fastSetTimeoutIncrementRef() - - nock.disableNetConnect() - - agent = setupMockedAgent() - collectorApi = new CollectorApi(agent) - - const redirectURL = helper.generateCollectorPath('preconnect') - failure = nock(URL).post(redirectURL).times(count).reply(503) - success = nock(URL) - .post(redirectURL) - .reply(200, { - return_value: { redirect_host: HOST, security_policies: {} } +const retryCounts = [1, 5] +for (const retryCount of retryCounts) { + test(`retry count: ${retryCount}`, async (t) => { + t.beforeEach(async (ctx) => { + ctx.nr = {} + + patchSetTimeout(ctx) + + const collector = new Collector({ runId: RUN_ID }) + ctx.nr.collector = collector + await collector.listen() + + let retries = 0 + collector.addHandler(helper.generateCollectorPath('preconnect'), (req, res) => { + if (retries < retryCount) { + retries += 1 + res.writeHead(503) + res.end() + return + } + res.json({ + return_value: { + redirect_host: `${collector.host}:${collector.port}`, + security_policies: {} + } }) - connection = nock(URL).post(helper.generateCollectorPath('connect')).reply(200, response) - }) - - t.afterEach(() => { - restoreSetTimeout() + }) - if (!nock.isDone()) { - /* eslint-disable no-console */ - console.error('Cleaning pending mocks: %j', nock.pendingMocks()) - /* eslint-enable no-console */ - nock.cleanAll() - } + const config = Object.assign({}, baseAgentConfig, collector.agentConfig, { + config: { run_id: RUN_ID } + }) + ctx.nr.agent = helper.loadMockedAgent(config) + ctx.nr.agent.reconfigure = function () {} + ctx.nr.agent.setState = function () {} - nock.enableNetConnect() - helper.unloadAgent(agent) + ctx.nr.collectorApi = new CollectorApi(ctx.nr.agent) }) - t.test('should not error out', (t) => { - testConnect(t, () => { - t.notOk(bad) - t.end() - }) + t.afterEach((ctx) => { + restoreTimeout(ctx) + helper.unloadAgent(ctx.nr.agent) + ctx.nr.collector.close() }) - t.test('should have a run ID', (t) => { - testConnect(t, () => { - t.equal(ssc.agent_run_id, RUN_ID) - t.end() + await t.test('should not error out', (t, end) => { + const { collectorApi } = t.nr + collectorApi.connect((error) => { + assert.equal(error, undefined) + end() }) }) - t.test('should pass through server-side configuration untouched', (t) => { - testConnect(t, () => { - t.same(ssc, valid) - t.end() + await t.test('should have a run ID', (t, end) => { + const { collectorApi } = t.nr + collectorApi.connect((error, res) => { + assert.equal(res.payload.agent_run_id, RUN_ID) + end() }) }) - function testConnect(t, cb) { + await t.test('should pass through server-side configuration untouched', (t, end) => { + const { collectorApi } = t.nr collectorApi.connect((error, res) => { - bad = error - ssc = res.payload - - t.ok(failure.isDone()) - t.ok(success.isDone()) - t.ok(connection.isDone()) - cb() + assert.deepStrictEqual(res.payload, { agent_run_id: RUN_ID }) + end() }) - } + }) }) -}) - -tap.test('disconnects on force disconnect (410)', (t) => { - t.autoend() - - let collectorApi = null - let agent = null +} +test('disconnects on force disconnect (410)', async (t) => { const exception = { exception: { message: 'fake force disconnect', @@ -345,61 +229,50 @@ tap.test('disconnects on force disconnect (410)', (t) => { } } - let disconnect = null - - t.beforeEach(() => { - fastSetTimeoutIncrementRef() - - nock.disableNetConnect() + t.beforeEach(async (ctx) => { + ctx.nr = {} - agent = setupMockedAgent() - collectorApi = new CollectorApi(agent) + const collector = new Collector({ runId: RUN_ID }) + ctx.nr.collector = collector + await collector.listen() - const redirectURL = helper.generateCollectorPath('preconnect') - disconnect = nock(URL).post(redirectURL).times(1).reply(410, exception) - }) - - t.afterEach(() => { - restoreSetTimeout() + collector.addHandler(helper.generateCollectorPath('preconnect'), (req, res) => { + res.json({ code: 410, payload: exception }) + }) - if (!nock.isDone()) { - /* eslint-disable no-console */ - console.error('Cleaning pending mocks: %j', nock.pendingMocks()) - /* eslint-enable no-console */ - nock.cleanAll() - } + const config = Object.assign({}, baseAgentConfig, collector.agentConfig, { + config: { run_id: RUN_ID } + }) + ctx.nr.agent = helper.loadMockedAgent(config) + ctx.nr.agent.reconfigure = function () {} + ctx.nr.agent.setState = function () {} - nock.enableNetConnect() - helper.unloadAgent(agent) + ctx.nr.collectorApi = new CollectorApi(ctx.nr.agent) }) - t.test('should not have errored', (t) => { - collectorApi.connect((err) => { - t.error(err) + t.afterEach(afterEach) - t.ok(disconnect.isDone()) - - t.end() + await t.test('should not have errored', (t, end) => { + const { collector, collectorApi } = t.nr + collectorApi.connect((error) => { + assert.equal(error, undefined) + assert.equal(collector.isDone('preconnect'), true) + end() }) }) - t.test('should not have a response body', (t) => { - collectorApi.connect((err, response) => { - t.notOk(response.payload) - - t.ok(disconnect.isDone()) - - t.end() + await t.test('should not have a response body', (t, end) => { + const { collector, collectorApi } = t.nr + collectorApi.connect((error, res) => { + assert.equal(res.payload, undefined) + assert.equal(collector.isDone('preconnect'), true) + end() }) }) }) -tap.test('retries preconnect until forced to disconnect (410)', (t) => { - t.autoend() - - let collectorApi = null - let agent = null - +test(`retries preconnect until forced to disconnect (410)`, async (t) => { + const retryCount = 500 const exception = { exception: { message: 'fake force disconnect', @@ -407,339 +280,315 @@ tap.test('retries preconnect until forced to disconnect (410)', (t) => { } } - let failure = null - let disconnect = null + t.beforeEach(async (ctx) => { + ctx.nr = {} - let capturedResponse = null + patchSetTimeout(ctx) - t.beforeEach(() => { - fastSetTimeoutIncrementRef() + const collector = new Collector({ runId: RUN_ID }) + ctx.nr.collector = collector + await collector.listen() - nock.disableNetConnect() + let retries = 0 + collector.addHandler(helper.generateCollectorPath('preconnect'), (req, res) => { + if (retries < retryCount) { + retries += 1 + res.writeHead(503) + res.end() + return + } + res.json({ code: 410, payload: exception }) + }) - agent = setupMockedAgent() - collectorApi = new CollectorApi(agent) + const config = Object.assign({}, baseAgentConfig, collector.agentConfig, { + config: { run_id: RUN_ID } + }) + ctx.nr.agent = helper.loadMockedAgent(config) + ctx.nr.agent.reconfigure = function () {} + ctx.nr.agent.setState = function () {} - const redirectURL = helper.generateCollectorPath('preconnect') - failure = nock(URL).post(redirectURL).times(500).reply(503) - disconnect = nock(URL).post(redirectURL).times(1).reply(410, exception) + ctx.nr.collectorApi = new CollectorApi(ctx.nr.agent) }) - t.afterEach(() => { - restoreSetTimeout() - - if (!nock.isDone()) { - /* eslint-disable no-console */ - console.error('Cleaning pending mocks: %j', nock.pendingMocks()) - /* eslint-enable no-console */ - nock.cleanAll() - } - - nock.enableNetConnect() - helper.unloadAgent(agent) + t.afterEach((ctx) => { + restoreTimeout(ctx) + helper.unloadAgent(ctx.nr.agent) + ctx.nr.collector.close() }) - t.test('should have received shutdown response', (t) => { - testConnect(t, () => { + await t.test('should have received shutdown response', (t, end) => { + const { collectorApi } = t.nr + collectorApi.connect((error, res) => { const shutdownCommand = CollectorResponse.AGENT_RUN_BEHAVIOR.SHUTDOWN - - t.ok(capturedResponse) - t.equal(capturedResponse.agentRun, shutdownCommand) - - t.end() + assert.deepStrictEqual(res.agentRun, shutdownCommand) + end() }) }) - - function testConnect(t, cb) { - collectorApi.connect((error, response) => { - capturedResponse = response - - t.ok(failure.isDone()) - t.ok(disconnect.isDone()) - cb() - }) - } }) -tap.test('retries on receiving invalid license key (401)', (t) => { - t.autoend() +test(`retries on receiving invalid license key (401)`, async (t) => { + const retryCount = 5 - let collectorApi = null - let agent = null + t.beforeEach(async (ctx) => { + ctx.nr = {} - let failure = null - let success = null - let connect = null + patchSetTimeout(ctx) - t.beforeEach(() => { - fastSetTimeoutIncrementRef() + const collector = new Collector({ runId: RUN_ID }) + ctx.nr.collector = collector + await collector.listen() - nock.disableNetConnect() + let retries = 0 + collector.addHandler(helper.generateCollectorPath('preconnect'), (req, res) => { + if (retries < retryCount) { + retries += 1 + res.writeHead(401) + res.end() + return + } + ctx.nr.retries = retries + res.json({ + return_value: {} + }) + }) + // We specify RUN_ID in the path so that we replace the existing connect + // handler with one that returns our unique run id. + collector.addHandler(helper.generateCollectorPath('connect', RUN_ID), (req, res) => { + res.json({ payload: { return_value: { agent_run_id: 31338 } } }) + }) - agent = setupMockedAgent() - collectorApi = new CollectorApi(agent) + const config = Object.assign({}, baseAgentConfig, collector.agentConfig, { + config: { run_id: RUN_ID } + }) + ctx.nr.agent = helper.loadMockedAgent(config) + ctx.nr.agent.reconfigure = function () {} + ctx.nr.agent.setState = function () {} - const preconnectURL = helper.generateCollectorPath('preconnect') - failure = nock(URL).post(preconnectURL).times(5).reply(401) - success = nock(URL).post(preconnectURL).reply(200, { return_value: {} }) - connect = nock(URL) - .post(helper.generateCollectorPath('connect')) - .reply(200, { return_value: { agent_run_id: 31338 } }) + ctx.nr.collectorApi = new CollectorApi(ctx.nr.agent) }) - t.afterEach(() => { - restoreSetTimeout() - - if (!nock.isDone()) { - /* eslint-disable no-console */ - console.error('Cleaning pending mocks: %j', nock.pendingMocks()) - /* eslint-enable no-console */ - nock.cleanAll() - } - - nock.enableNetConnect() - helper.unloadAgent(agent) + t.afterEach((ctx) => { + restoreTimeout(ctx) + helper.unloadAgent(ctx.nr.agent) + ctx.nr.collector.close() }) - t.test('should call the expected number of times', (t) => { - testConnect(t, () => { - t.end() + await t.test('should call the expected number of times', (t, end) => { + const { collectorApi } = t.nr + collectorApi.connect((error, res) => { + assert.equal(t.nr.retries, 5) + assert.equal(res.payload.agent_run_id, 31338) + end() }) }) - - function testConnect(t, cb) { - collectorApi.connect(() => { - t.ok(failure.isDone()) - t.ok(success.isDone()) - t.ok(connect.isDone()) - - cb() - }) - } }) -tap.test('retries on misconfigured proxy', (t) => { - const sandbox = sinon.createSandbox() - const loggerMock = require('../mocks/logger')(sandbox) - const CollectorApiTest = proxyquire('../../../lib/collector/api', { - '../logger': { - child: sandbox.stub().callsFake(() => loggerMock) - } - }) - t.autoend() - - let collectorApi = null - let agent = null - - const error = { - code: 'EPROTO' - } - - let failure = null - let success = null - let connect = null - - t.beforeEach(() => { - fastSetTimeoutIncrementRef() - +test(`retries on misconfigured proxy`, async (t) => { + // We are using `nock` for these tests because it provides its own socket + // implementation that is able to fake a bad connection to a server. + // Basically, these tests are attempting to verify conditions around + // establishing connections to a proxy server, and we need to be able to + // simulate those connections not establishing correctly. The best we can + // do with our in-process HTTP server is to generate an abruptly closed + // request, but that will not meet the "is misconfigured proxy" assertion + // the agent uses. We'd like a better way of dealing with this, but for now + // (2024-08), we are moving on so that this does not block our conversion + // from `tap` to `node:test`. + // + // See https://github.com/nock/nock/blob/66eb7f48a7bdf50ee79face6403326b02d23253b/lib/socket.js#L81-L88. + // That `destroy` method is what ends up implementing the functionality + // behind `nock.replyWithError`. + + const expectedError = { code: 'EPROTO' } + + t.beforeEach(async (ctx) => { + ctx.nr = {} + + patchSetTimeout(ctx) nock.disableNetConnect() - agent = setupMockedAgent() - agent.config.proxy_port = '8080' - agent.config.proxy_host = 'test-proxy-server' - collectorApi = new CollectorApiTest(agent) + ctx.nr.agent = helper.loadMockedAgent({ + host: 'collector.newrelic.com', + port: 443, + ...baseAgentConfig + }) + ctx.nr.agent.reconfigure = function () {} + ctx.nr.agent.setState = function () {} + ctx.nr.agent.config.proxy_port = '8080' + ctx.nr.agent.config.proxy_host = 'test-proxy-server' + const baseURL = 'https://collector.newrelic.com' const preconnectURL = helper.generateCollectorPath('preconnect') - failure = nock(URL).post(preconnectURL).times(1).replyWithError(error) - success = nock(URL).post(preconnectURL).reply(200, { return_value: {} }) - connect = nock(URL) + ctx.nr.failure = nock(baseURL).post(preconnectURL).times(1).replyWithError(expectedError) + ctx.nr.success = nock(baseURL).post(preconnectURL).reply(200, { return_value: {} }) + ctx.nr.connect = nock(baseURL) .post(helper.generateCollectorPath('connect')) .reply(200, { return_value: { agent_run_id: 31338 } }) - }) - - t.afterEach(() => { - sandbox.resetHistory() - restoreSetTimeout() - if (!nock.isDone()) { - /* eslint-disable no-console */ - console.error('Cleaning pending mocks: %j', nock.pendingMocks()) - /* eslint-enable no-console */ - nock.cleanAll() - } + ctx.nr.logs = [] + const CAPI = proxyquire('../../../lib/collector/api', { + '../logger': { + child() { + return this + }, + debug() {}, + error() {}, + info() {}, + warn(...args) { + ctx.nr.logs.push(args) + }, + trace() {} + } + }) + ctx.nr.collectorApi = new CAPI(ctx.nr.agent) + }) + t.afterEach((ctx) => { + restoreTimeout(ctx) + helper.unloadAgent(ctx.nr.agent) nock.enableNetConnect() - helper.unloadAgent(agent) }) - t.test('should log warning when proxy is misconfigured', (t) => { - collectorApi.connect(() => { - t.ok(failure.isDone()) - t.ok(success.isDone()) - t.ok(connect.isDone()) + await t.test('should log warning when proxy is misconfigured', (t, end) => { + const { collectorApi } = t.nr + collectorApi.connect((error, res) => { + assert.equal(t.nr.failure.isDone(), true) + assert.equal(t.nr.success.isDone(), true) + assert.equal(t.nr.connect.isDone(), true) + assert.equal(res.payload.agent_run_id, 31338) const expectErrorMsg = 'Your proxy server appears to be configured to accept connections \ over http. When setting `proxy_host` and `proxy_port` New Relic attempts to connect over \ SSL(https). If your proxy is configured to accept connections over http, try setting `proxy` \ to a fully qualified URL(e.g http://proxy-host:8080).' + assert.deepStrictEqual( + t.nr.logs, + [[expectedError, expectErrorMsg]], + 'Proxy misconfigured message correct' + ) - t.same(loggerMock.warn.args, [[error, expectErrorMsg]], 'Proxy misconfigured message correct') - t.end() + end() }) }) - t.test('should not log warning when proxy is configured properly but still get EPROTO', (t) => { - collectorApi._agent.config.proxy = 'http://test-proxy-server:8080' - collectorApi.connect(() => { - t.ok(failure.isDone()) - t.ok(success.isDone()) - t.ok(connect.isDone()) - t.same(loggerMock.warn.args, [], 'Proxy misconfigured message not logged') - t.end() - }) - }) -}) - -tap.test('in a LASP/CSP enabled agent', (t) => { - const SECURITY_POLICIES_TOKEN = 'TEST-TEST-TEST-TEST' - - t.autoend() - - let agent = null - let collectorApi = null - let policies = null - - t.beforeEach(() => { - agent = setupMockedAgent() - agent.config.security_policies_token = SECURITY_POLICIES_TOKEN - - collectorApi = new CollectorApi(agent) + await t.test( + 'should not log warning when proxy is configured properly but still get EPROTO', + (t, end) => { + const { collectorApi } = t.nr + collectorApi._agent.config.proxy = 'http://test-proxy-server:8080' + collectorApi.connect((error, res) => { + assert.equal(t.nr.failure.isDone(), true) + assert.equal(t.nr.success.isDone(), true) + assert.equal(t.nr.connect.isDone(), true) + assert.equal(res.payload.agent_run_id, 31338) - policies = securityPolicies() + assert.deepStrictEqual(t.nr.logs, [], 'Proxy misconfigured message not logged') - nock.disableNetConnect() - }) - - t.afterEach(() => { - if (!nock.isDone()) { - /* eslint-disable no-console */ - console.error('Cleaning pending mocks: %j', nock.pendingMocks()) - /* eslint-enable no-console */ - nock.cleanAll() + end() + }) } + ) +}) - nock.enableNetConnect() - - helper.unloadAgent(agent) - agent = null - collectorApi = null - policies = null - }) +test('in a LASP/CSP enabled agent', async (t) => { + const SECURITY_POLICIES_TOKEN = 'TEST-TEST-TEST-TEST' - t.test('should include security policies in api callback response', (t) => { - const valid = { - agent_run_id: RUN_ID, - security_policies: policies - } + t.beforeEach(async (ctx) => { + ctx.nr = {} - const response = { return_value: valid } + const collector = new Collector({ runId: RUN_ID }) + ctx.nr.collector = collector + await collector.listen() - const redirection = nock(URL + ':443') - .post(helper.generateCollectorPath('preconnect')) - .reply(200, { - return_value: { - redirect_host: HOST, - security_policies: policies + const config = Object.assign({}, baseAgentConfig, collector.agentConfig, { + config: { run_id: RUN_ID } + }) + ctx.nr.agent = helper.loadMockedAgent(config) + ctx.nr.agent.reconfigure = function () {} + ctx.nr.agent.setState = function () {} + ctx.nr.agent.config.security_policies_token = SECURITY_POLICIES_TOKEN + + ctx.nr.collectorApi = new CollectorApi(ctx.nr.agent) + ctx.nr.policies = securityPolicies() + + ctx.nr.validResponse = { agent_run_id: RUN_ID, security_policies: ctx.nr.policies } + collector.addHandler(helper.generateCollectorPath('preconnect'), (req, res) => { + res.json({ + payload: { + return_value: { + redirect_host: `https://${collector.host}:${collector.port}`, + security_policies: ctx.nr.policies + } } }) + }) + collector.addHandler(helper.generateCollectorPath('connect'), (req, res) => { + res.json({ payload: { return_value: ctx.nr.validResponse } }) + }) + }) - const connection = nock(URL).post(helper.generateCollectorPath('connect')).reply(200, response) - - collectorApi.connect(function test(error, res) { - t.same(res.payload, valid) - - redirection.done() - connection.done() + t.afterEach(afterEach) - t.end() + await t.test('should include security policies in api callback response', (t, end) => { + const { collectorApi } = t.nr + collectorApi.connect((error, res) => { + assert.equal(error, undefined) + assert.deepStrictEqual(res.payload, t.nr.validResponse) + end() }) }) - t.test('drops data collected before connect when policies are updated', (t) => { + await t.test('drops data collected before connect when policies are update', (t, end) => { + const { agent, collectorApi } = t.nr agent.config.api.custom_events_enabled = true - agent.customEventAggregator.add(['will be overwritten']) - t.equal(agent.customEventAggregator.length, 1) - - const valid = { - agent_run_id: RUN_ID, - security_policies: policies - } - - const response = { return_value: valid } - - const redirection = nock(URL + ':443') - .post(helper.generateCollectorPath('preconnect')) - .reply(200, { - return_value: { - redirect_host: HOST, - security_policies: policies - } - }) + assert.equal(agent.customEventAggregator.length, 1) + collectorApi.connect((error, res) => { + assert.equal(error, undefined) + assert.deepStrictEqual(res.payload, t.nr.validResponse) + assert.equal(agent.customEventAggregator.length, 0) + end() + }) + }) +}) - const connection = nock(URL).post(helper.generateCollectorPath('connect')).reply(200, response) +async function beforeEach(ctx) { + ctx.nr = {} - collectorApi.connect(function test(error, res) { - t.same(res.payload, valid) + const collector = new Collector({ runId: RUN_ID }) + ctx.nr.collector = collector + await collector.listen() - t.equal(agent.customEventAggregator.length, 0) + const config = Object.assign({}, baseAgentConfig, collector.agentConfig, { + config: { run_id: RUN_ID } + }) + ctx.nr.agent = helper.loadMockedAgent(config) + ctx.nr.agent.reconfigure = function () {} + ctx.nr.agent.setState = function () {} - redirection.done() - connection.done() + ctx.nr.collectorApi = new CollectorApi(ctx.nr.agent) +} - t.end() - }) - }) -}) +function afterEach(ctx) { + helper.unloadAgent(ctx.nr.agent) + ctx.nr.collector.close() +} -function fastSetTimeoutIncrementRef() { +function patchSetTimeout(ctx) { + ctx.nr.setTimeout = global.setTimeout global.setTimeout = function (cb) { - const nodeTimeout = timeout(cb, 0) + const nodeTimeout = ctx.nr.setTimeout(cb, 0) - // This is a hack to keep tap from shutting down test early. - // Is there a better way to do this? + // This is a hack to keep the test runner from reaping the test before + // the retries are complete. Is there a better way to do this? setImmediate(() => { nodeTimeout.ref() }) - return nodeTimeout } } -function restoreSetTimeout() { - global.setTimeout = timeout -} - -function setupMockedAgent() { - const agent = helper.loadMockedAgent({ - host: HOST, - port: PORT, - app_name: ['TEST'], - ssl: true, - license_key: 'license key here', - utilization: { - detect_aws: false, - detect_pcf: false, - detect_azure: false, - detect_gcp: false, - detect_docker: false - }, - browser_monitoring: {}, - transaction_tracer: {} - }) - agent.reconfigure = function () {} - agent.setState = function () {} - - return agent +function restoreTimeout(ctx) { + global.setTimeout = ctx.nr.setTimeout } diff --git a/test/unit/collector/api-login.test.js b/test/unit/collector/api-login.test.js index e4e9f5dfb1..b94ff3b410 100644 --- a/test/unit/collector/api-login.test.js +++ b/test/unit/collector/api-login.test.js @@ -1,812 +1,552 @@ /* - * 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 tap = require('tap') -const nock = require('nock') +const test = require('node:test') +const assert = require('node:assert') +const promiseResolvers = require('../../lib/promise-resolvers') +const Collector = require('../../lib/test-collector') const helper = require('../../lib/agent_helper') +const { securityPolicies } = require('../../lib/fixtures') const CollectorApi = require('../../../lib/collector/api') -const securityPolicies = require('../../lib/fixtures').securityPolicies -const HOST = 'collector.newrelic.com' -const PORT = 8080 -const URL = 'https://' + HOST const RUN_ID = 1337 +const SECURITY_POLICIES_TOKEN = 'TEST-TEST-TEST-TEST' +const baseAgentConfig = { + app_name: ['TEST'], + ssl: true, + license_key: 'license key here', + utilization: { + detect_aws: false, + detect_pcf: false, + detect_azure: false, + detect_gcp: false, + detect_docker: false + }, + browser_monitoring: {}, + transaction_tracer: {} +} -tap.test('when high_security: true', (t) => { - t.autoend() - - let agent = null - let collectorApi = null - - t.beforeEach(() => { - agent = setupMockedAgent() - agent.config.high_security = true - - collectorApi = new CollectorApi(agent) - - nock.disableNetConnect() - }) +test('when high_security: true', async (t) => { + t.beforeEach(async (ctx) => { + ctx.nr = {} - t.afterEach(() => { - if (!nock.isDone()) { - /* eslint-disable no-console */ - console.error('Cleaning pending mocks: %j', nock.pendingMocks()) - /* eslint-enable no-console */ - nock.cleanAll() - } + const collector = new Collector({ runId: RUN_ID }) + ctx.nr.collector = collector + await collector.listen() - nock.enableNetConnect() + const config = Object.assign({}, baseAgentConfig, collector.agentConfig, { + config: { run_id: RUN_ID } + }) + ctx.nr.agent = helper.loadMockedAgent(config) + ctx.nr.agent.reconfigure = function () {} + ctx.nr.agent.setState = function () {} + ctx.nr.agent.config.high_security = true - helper.unloadAgent(agent) - agent = null - collectorApi = null + ctx.nr.collectorApi = new CollectorApi(ctx.nr.agent) }) - t.test('should send high_security:true in preconnect payload', (t) => { - const expectedPreconnectBody = [{ high_security: true }] - - const preconnect = nock(URL + ':8080') - .post(helper.generateCollectorPath('preconnect'), expectedPreconnectBody) - .reply(200, { - return_value: { - redirect_host: HOST - } - }) - - const connectResponse = { return_value: { agent_run_id: RUN_ID } } - const connect = nock(URL) - .post(helper.generateCollectorPath('connect')) - .reply(200, connectResponse) + t.afterEach(afterEach) - collectorApi._login(function test(err) { - // Request will only be successful if body matches expected - t.error(err) - - preconnect.done() - connect.done() - t.end() + await t.test('should send high_security:true in preconnect payload', (t, end) => { + const { collector, collectorApi } = t.nr + let handled = false // effectively a `t.plan` (which we don't have in Node 18) + collector.addHandler(helper.generateCollectorPath('preconnect'), async (req, res) => { + const body = JSON.parse(await req.body()) + assert.equal(body[0].high_security, true) + handled = true + collector.preconnectHandler(req, res) + }) + collectorApi._login((error) => { + // Request will only be successful if body matches expected payload. + assert.equal(error, undefined) + assert.equal(handled, true) + end() }) }) }) -tap.test('when high_security: false', (t) => { - t.autoend() - - let agent = null - let api = null +test('when high_security: false', async (t) => { + t.beforeEach(async (ctx) => { + ctx.nr = {} - t.beforeEach(() => { - agent = setupMockedAgent() - agent.config.high_security = false - - api = new CollectorApi(agent) - - nock.disableNetConnect() - }) + const collector = new Collector({ runId: RUN_ID }) + ctx.nr.collector = collector + await collector.listen() - t.afterEach(() => { - if (!nock.isDone()) { - /* eslint-disable no-console */ - console.error('Cleaning pending mocks: %j', nock.pendingMocks()) - /* eslint-enable no-console */ - nock.cleanAll() - } - - nock.enableNetConnect() + const config = Object.assign({}, baseAgentConfig, collector.agentConfig, { + config: { run_id: RUN_ID } + }) + ctx.nr.agent = helper.loadMockedAgent(config) + ctx.nr.agent.reconfigure = function () {} + ctx.nr.agent.setState = function () {} + ctx.nr.agent.config.high_security = false - helper.unloadAgent(agent) - agent = null - api = null + ctx.nr.collectorApi = new CollectorApi(ctx.nr.agent) }) - t.test('should send high_security:true in preconnect payload', (t) => { - const expectedPreconnectBody = [{ high_security: false }] + t.afterEach(afterEach) - const preconnect = nock(URL + ':8080') - .post(helper.generateCollectorPath('preconnect'), expectedPreconnectBody) - .reply(200, { - return_value: { - redirect_host: HOST - } - }) - - const connectResponse = { return_value: { agent_run_id: RUN_ID } } - const connect = nock(URL) - .post(helper.generateCollectorPath('connect')) - .reply(200, connectResponse) - - api._login(function test(err) { - // Request will only be successful if body matches expected - t.error(err) - - preconnect.done() - connect.done() - t.end() + await t.test('should send high_security:false in preconnect payload', (t, end) => { + const { collector, collectorApi } = t.nr + let handled = false // effectively a `t.plan` (which we don't have in Node 18) + collector.addHandler(helper.generateCollectorPath('preconnect'), async (req, res) => { + const body = JSON.parse(await req.body()) + assert.equal(body[0].high_security, false) + handled = true + collector.preconnectHandler(req, res) + }) + collectorApi._login((error) => { + // Request will only be successful if body matches expected payload. + assert.equal(error, undefined) + assert.equal(handled, true) + end() }) }) }) -tap.test('in a LASP-enabled agent', (t) => { - const SECURITY_POLICIES_TOKEN = 'TEST-TEST-TEST-TEST' - - t.autoend() - - let agent = null - let collectorApi = null - let policies = null +test('in a LASP-enabled agent', async (t) => { + t.beforeEach(async (ctx) => { + ctx.nr = {} - t.beforeEach(() => { - agent = setupMockedAgent() - agent.config.security_policies_token = SECURITY_POLICIES_TOKEN + const collector = new Collector({ runId: RUN_ID }) + ctx.nr.collector = collector + await collector.listen() - collectorApi = new CollectorApi(agent) + const config = Object.assign({}, baseAgentConfig, collector.agentConfig, { + config: { run_id: RUN_ID } + }) + ctx.nr.agent = helper.loadMockedAgent(config) + ctx.nr.agent.reconfigure = function () {} + ctx.nr.agent.setState = function () {} + ctx.nr.agent.config.security_policies_token = SECURITY_POLICIES_TOKEN - policies = securityPolicies() + ctx.nr.policies = securityPolicies() - nock.disableNetConnect() + ctx.nr.collectorApi = new CollectorApi(ctx.nr.agent) }) - t.afterEach(() => { - if (!nock.isDone()) { - /* eslint-disable no-console */ - console.error('Cleaning pending mocks: %j', nock.pendingMocks()) - /* eslint-enable no-console */ - nock.cleanAll() - } - - nock.enableNetConnect() - - helper.unloadAgent(agent) - agent = null - collectorApi = null - policies = null - }) - - // HSM should never be true when LASP/CSP enabled but payload should still be sent. - t.test('should send token in preconnect payload with high_security:false', (t) => { - const expectedPreconnectBody = [ - { - security_policies_token: SECURITY_POLICIES_TOKEN, - high_security: false - } - ] - - const preconnect = nock(URL + ':8080') - .post(helper.generateCollectorPath('preconnect'), expectedPreconnectBody) - .reply(200, { - return_value: { - redirect_host: HOST, - security_policies: {} - } - }) - - collectorApi._login(function test(err) { - // Request will only be successful if body matches expected - t.error(err) + t.afterEach(afterEach) - preconnect.done() - t.end() + await t.test('should send token in preconnect payload with high_security:false', (t, end) => { + // HSM should never be true when LASP/CSP enabled but payload should still be sent. + const { collector, collectorApi } = t.nr + let handled = false + collector.addHandler(helper.generateCollectorPath('preconnect'), async (req, res) => { + const body = JSON.parse(await req.body()) + assert.equal(body[0].security_policies_token, SECURITY_POLICIES_TOKEN) + assert.equal(body[0].high_security, false) + handled = true + collector.preconnectHandler(req, res) + }) + collectorApi._login((error) => { + assert.equal(error, undefined) + assert.equal(handled, true) + end() }) }) - t.test('should fail if preconnect res is missing expected policies', (t) => { - const redirection = nock(URL + ':8080') - .post(helper.generateCollectorPath('preconnect')) - .reply(200, { - return_value: { - redirect_host: HOST, - security_policies: {} - } - }) - - collectorApi._login(function test(err, response) { - t.error(err) - t.equal(response.shouldShutdownRun(), true) - - redirection.done() - t.end() + await t.test('should fail if preconnect res is missing expected policies', (t, end) => { + const { collector, collectorApi } = t.nr + collectorApi._login((error, res) => { + assert.equal(error, undefined) + assert.equal(res.shouldShutdownRun(), true) + assert.equal(collector.isDone('preconnect'), true) + end() }) }) - t.test('should fail if agent is missing required policy', (t) => { - policies.test = { required: true } - - const redirection = nock(URL + ':8080') - .post(helper.generateCollectorPath('preconnect')) - .reply(200, { - return_value: { - redirect_host: HOST, - security_policies: policies + await t.test('should fail if agent is missing required property', (t, end) => { + const { collector, collectorApi } = t.nr + t.nr.policies.test = { required: true } + collector.addHandler(helper.generateCollectorPath('preconnect'), (req, res) => { + res.json({ + payload: { + return_value: { + redirect_host: `${collector.host}:${collector.port}`, + security_policies: t.nr.policies + } } }) - - collectorApi._login(function test(err, response) { - t.error(err) - t.equal(response.shouldShutdownRun(), true) - - redirection.done() - t.end() + }) + collectorApi._login((error, res) => { + assert.equal(error, undefined) + assert.equal(res.shouldShutdownRun(), true) + assert.equal(collector.isDone('preconnect'), true) + end() }) }) }) -tap.test('should copy request headers', (t) => { - let agent = null - let collectorApi = null - - agent = setupMockedAgent() - collectorApi = new CollectorApi(agent) - - nock.disableNetConnect() - - t.teardown(() => { - if (!nock.isDone()) { - /* eslint-disable no-console */ - console.error('Cleaning pending mocks: %j', nock.pendingMocks()) - /* eslint-enable no-console */ - nock.cleanAll() - } - - nock.enableNetConnect() - - helper.unloadAgent(agent) - agent = null - collectorApi = null +test('should copy request headers', async (t) => { + const { promise, resolve } = promiseResolvers() + await beforeEach(t) + t.after(async () => { + await afterEach(t) }) - const reqHeaderMap = { - 'X-NR-TEST-HEADER': 'TEST VALUE' - } - - const valid = { + const { collector, collectorApi } = t.nr + const validResponse = { agent_run_id: RUN_ID, - request_headers_map: reqHeaderMap - } - - const response = { return_value: valid } - - const redirection = nock(URL + ':8080') - .post(helper.generateCollectorPath('preconnect')) - .reply(200, { return_value: { redirect_host: HOST, security_policies: {} } }) - - const connection = nock(URL).post(helper.generateCollectorPath('connect')).reply(200, response) - - collectorApi._login(function test() { - t.same(collectorApi._reqHeadersMap, reqHeaderMap) - redirection.done() - connection.done() - - t.end() - }) -}) - -tap.test('receiving 200 response, with valid data', (t) => { - t.autoend() - - let agent = null - let collectorApi = null - - let redirection = null - let connection = null - - const validSsc = { - agent_run_id: RUN_ID + request_headers_map: { + 'X-NR-TEST-HEADER': 'TEST VALUE' + } } - - t.beforeEach(() => { - agent = setupMockedAgent() - collectorApi = new CollectorApi(agent) - - nock.disableNetConnect() - - const response = { return_value: validSsc } - - redirection = nock(URL + ':8080') - .post(helper.generateCollectorPath('preconnect')) - .reply(200, { return_value: { redirect_host: HOST, security_policies: {} } }) - connection = nock(URL).post(helper.generateCollectorPath('connect')).reply(200, response) + collector.addHandler(helper.generateCollectorPath('connect', RUN_ID), (req, res) => { + res.json({ payload: { return_value: validResponse } }) }) - t.afterEach(() => { - if (!nock.isDone()) { - /* eslint-disable no-console */ - console.error('Cleaning pending mocks: %j', nock.pendingMocks()) - /* eslint-enable no-console */ - nock.cleanAll() - } - - nock.enableNetConnect() - - helper.unloadAgent(agent) - agent = null - collectorApi = null + collectorApi._login(() => { + assert.equal(collectorApi._reqHeadersMap['X-NR-TEST-HEADER'], 'TEST VALUE') + resolve() }) - t.test('should not error out', (t) => { - collectorApi._login(function test(error) { - t.error(error) + await promise +}) - redirection.done() - connection.done() +test('receiving 200 response, with valid data', async (t) => { + t.beforeEach(beforeEach) + t.afterEach(afterEach) - t.end() + await t.test('should not error out', (t, end) => { + const { collectorApi } = t.nr + collectorApi._login((error) => { + assert.equal(error, undefined) + end() }) }) - t.test('should have a run ID', (t) => { - collectorApi._login(function test(error, res) { - const ssc = res.payload - t.equal(ssc.agent_run_id, RUN_ID) - - redirection.done() - connection.done() - - t.end() + await t.test('should have a run ID', (t, end) => { + const { collectorApi } = t.nr + collectorApi._login((error, res) => { + assert.equal(error, undefined) + assert.equal(res.payload.agent_run_id, RUN_ID) + end() }) }) - t.test('should pass through server-side configuration untouched', (t) => { - collectorApi._login(function test(error, res) { - const ssc = res.payload - t.same(ssc, validSsc) - - redirection.done() - connection.done() - - t.end() + await t.test('should pass through server-side configuration untouched', (t, end) => { + const { collectorApi } = t.nr + collectorApi._login((error, res) => { + assert.equal(error, undefined) + assert.deepStrictEqual(res.payload, { agent_run_id: RUN_ID }) + end() }) }) }) -tap.test('receiving 503 response from preconnect', (t) => { - t.autoend() - - let agent = null - let collectorApi = null +test('receiving 503 response from preconnect', async (t) => { + t.beforeEach(async (ctx) => { + ctx.nr = {} - let redirection = null + const collector = new Collector({ runId: RUN_ID }) + ctx.nr.collector = collector + await collector.listen() - t.beforeEach(() => { - agent = setupMockedAgent() - collectorApi = new CollectorApi(agent) - - nock.disableNetConnect() - - redirection = redirection = nock(URL + ':8080') - .post(helper.generateCollectorPath('preconnect')) - .reply(503) - }) - - t.afterEach(() => { - if (!nock.isDone()) { - /* eslint-disable no-console */ - console.error('Cleaning pending mocks: %j', nock.pendingMocks()) - /* eslint-enable no-console */ - nock.cleanAll() - } + collector.addHandler(helper.generateCollectorPath('preconnect'), (req, res) => { + res.writeHead(503) + res.end() + }) - nock.enableNetConnect() + const config = Object.assign({}, baseAgentConfig, collector.agentConfig, { + config: { run_id: RUN_ID } + }) + ctx.nr.agent = helper.loadMockedAgent(config) + ctx.nr.agent.reconfigure = function () {} + ctx.nr.agent.setState = function () {} - helper.unloadAgent(agent) - agent = null - collectorApi = null + ctx.nr.collectorApi = new CollectorApi(ctx.nr.agent) }) - t.test('should not have gotten an error', (t) => { - collectorApi._login(function test(error) { - t.error(error) - redirection.done() + t.afterEach(afterEach) - t.end() + await t.test('should not have gotten an error', (t, end) => { + const { collectorApi } = t.nr + collectorApi._login((error) => { + assert.equal(error, undefined) + end() }) }) - t.test('should have passed on the status code', (t) => { - collectorApi._login(function test(error, response) { - t.error(error) - redirection.done() - - t.equal(response.status, 503) - - t.end() + await t.test('should have passed on the status code', (t, end) => { + const { collectorApi } = t.nr + collectorApi._login((error, res) => { + assert.equal(error, undefined) + assert.equal(res.status, 503) + end() }) }) }) -tap.test('receiving no hostname from preconnect', (t) => { - t.autoend() - - let agent = null - let collectorApi = null - - let redirection = null - let connection = null - - const validSsc = { - agent_run_id: RUN_ID - } - - t.beforeEach(() => { - agent = setupMockedAgent() - collectorApi = new CollectorApi(agent) - - nock.disableNetConnect() - - const response = { return_value: validSsc } +test('receiving no hostname from preconnect', async (t) => { + t.beforeEach(async (ctx) => { + ctx.nr = {} + + const collector = new Collector({ runId: RUN_ID }) + ctx.nr.collector = collector + await collector.listen() + + collector.addHandler(helper.generateCollectorPath('preconnect'), (req, res) => { + res.json({ + payload: { + return_value: { + redirect_host: '', + security_policies: {} + } + } + }) + }) - redirection = nock(URL + ':8080') - .post(helper.generateCollectorPath('preconnect')) - .reply(200, { return_value: { redirect_host: '', security_policies: {} } }) + const config = Object.assign({}, baseAgentConfig, collector.agentConfig, { + config: { run_id: RUN_ID } + }) + ctx.nr.agent = helper.loadMockedAgent(config) + ctx.nr.agent.reconfigure = function () {} + ctx.nr.agent.setState = function () {} - connection = nock(URL + ':8080') - .post(helper.generateCollectorPath('connect')) - .reply(200, response) + ctx.nr.collectorApi = new CollectorApi(ctx.nr.agent) }) - t.afterEach(() => { - if (!nock.isDone()) { - /* eslint-disable no-console */ - console.error('Cleaning pending mocks: %j', nock.pendingMocks()) - /* eslint-enable no-console */ - nock.cleanAll() - } - - nock.enableNetConnect() + t.afterEach(afterEach) - helper.unloadAgent(agent) - agent = null - collectorApi = null + await t.test('should not error out', (t, end) => { + const { collectorApi } = t.nr + collectorApi._login((error) => { + assert.equal(error, undefined) + end() + }) }) - t.test('should not error out', (t) => { - collectorApi._login(function test(error) { - t.error(error) - - redirection.done() - connection.done() - - t.end() + await t.test('should not error out', (t, end) => { + const { collectorApi } = t.nr + collectorApi._login((error) => { + assert.equal(error, undefined) + end() }) }) - t.test('should use preexisting collector hostname', (t) => { - collectorApi._login(function test() { - t.equal(agent.config.host, HOST) - - redirection.done() - connection.done() - - t.end() + await t.test('should use preexisting collector hostname', (t, end) => { + const { agent, collectorApi } = t.nr + collectorApi._login((error) => { + assert.equal(error, undefined) + assert.equal(agent.config.host, '127.0.0.1') + end() }) }) - t.test('should pass along server-side configuration from collector', (t) => { - collectorApi._login(function test(error, res) { - const ssc = res.payload - t.equal(ssc.agent_run_id, RUN_ID) - - redirection.done() - connection.done() - - t.end() + await t.test('should pass along server-side configuration from collector', (t, end) => { + const { collectorApi } = t.nr + collectorApi._login((error, res) => { + assert.equal(error, undefined) + assert.equal(res.payload.agent_run_id, RUN_ID) + end() }) }) }) -tap.test('receiving a weirdo redirect name from preconnect', (t) => { - t.autoend() - - let agent = null - let collectorApi = null - - let redirection = null - let connection = null - - const validSsc = { - agent_run_id: RUN_ID - } - - t.beforeEach(() => { - agent = setupMockedAgent() - collectorApi = new CollectorApi(agent) - - nock.disableNetConnect() - - const response = { return_value: validSsc } - - redirection = nock(URL + ':8080') - .post(helper.generateCollectorPath('preconnect')) - .reply(200, { - return_value: { - redirect_host: HOST + ':chug:8089', - security_policies: {} +test('receiving a weirdo redirect name from preconnect', async (t) => { + t.beforeEach(async (ctx) => { + ctx.nr = {} + + const collector = new Collector({ runId: RUN_ID }) + ctx.nr.collector = collector + await collector.listen() + + collector.addHandler(helper.generateCollectorPath('preconnect'), (req, res) => { + res.json({ + payload: { + return_value: { + redirect_host: `${collector.host}:chug:${collector.port}`, + security_policies: {} + } } }) + }) - connection = nock(URL + ':8080') - .post(helper.generateCollectorPath('connect')) - .reply(200, response) - }) - - t.afterEach(() => { - if (!nock.isDone()) { - /* eslint-disable no-console */ - console.error('Cleaning pending mocks: %j', nock.pendingMocks()) - /* eslint-enable no-console */ - nock.cleanAll() - } - - nock.enableNetConnect() - - helper.unloadAgent(agent) - agent = null - collectorApi = null - }) - - t.test('should not error out', (t) => { - collectorApi._login(function test(error) { - t.error(error) - - redirection.done() - connection.done() - - t.end() + const config = Object.assign({}, baseAgentConfig, collector.agentConfig, { + config: { run_id: RUN_ID } }) - }) + ctx.nr.agent = helper.loadMockedAgent(config) + ctx.nr.agent.reconfigure = function () {} + ctx.nr.agent.setState = function () {} - t.test('should use preexisting collector hostname', (t) => { - collectorApi._login(function test() { - t.equal(agent.config.host, HOST) + ctx.nr.collectorApi = new CollectorApi(ctx.nr.agent) + }) - redirection.done() - connection.done() + t.afterEach(afterEach) - t.end() + await t.test('should not error out', (t, end) => { + const { collectorApi } = t.nr + collectorApi._login((error) => { + assert.equal(error, undefined) + end() }) }) - t.test('should use preexisting collector port number', (t) => { - collectorApi._login(function test() { - t.equal(agent.config.port, PORT) - - redirection.done() - connection.done() - - t.end() + await t.test('should use preexisting collector hostname and port', (t, end) => { + const { agent, collector, collectorApi } = t.nr + collectorApi._login((error) => { + assert.equal(error, undefined) + assert.equal(agent.config.host, collector.host) + assert.equal(agent.config.port, collector.port) + end() }) }) - t.test('should pass along server-side configuration from collector', (t) => { - collectorApi._login(function test(error, res) { - const ssc = res.payload - t.equal(ssc.agent_run_id, RUN_ID) - - redirection.done() - connection.done() - - t.end() + await t.test('should pass along server-side configuration from collector', (t, end) => { + const { collectorApi } = t.nr + collectorApi._login((error, res) => { + assert.equal(error, undefined) + assert.equal(res.payload.agent_run_id, RUN_ID) + end() }) }) }) -tap.test('receiving no config back from connect', (t) => { - t.autoend() - - let agent = null - let collectorApi = null - - let redirection = null - let connection = null +test('receiving no config back from connect', async (t) => { + t.beforeEach(async (ctx) => { + ctx.nr = {} - t.beforeEach(() => { - agent = setupMockedAgent() - collectorApi = new CollectorApi(agent) + const collector = new Collector({ runId: RUN_ID }) + ctx.nr.collector = collector + await collector.listen() - nock.disableNetConnect() - - redirection = nock(URL + ':8080') - .post(helper.generateCollectorPath('preconnect')) - .reply(200, { return_value: { redirect_host: HOST, security_policies: {} } }) - - connection = nock(URL) - .post(helper.generateCollectorPath('connect')) - .reply(200, { return_value: null }) - }) - - t.afterEach(() => { - if (!nock.isDone()) { - /* eslint-disable no-console */ - console.error('Cleaning pending mocks: %j', nock.pendingMocks()) - /* eslint-enable no-console */ - nock.cleanAll() - } - - nock.enableNetConnect() - - helper.unloadAgent(agent) - agent = null - collectorApi = null - }) - - t.test('should have gotten an error', (t) => { - collectorApi._login(function test(error) { - t.ok(error) - - redirection.done() - connection.done() + collector.addHandler(helper.generateCollectorPath('connect'), (req, res) => { + res.json({ + payload: { + return_value: null + } + }) + }) - t.end() + const config = Object.assign({}, baseAgentConfig, collector.agentConfig, { + config: { run_id: RUN_ID } }) - }) + ctx.nr.agent = helper.loadMockedAgent(config) + ctx.nr.agent.reconfigure = function () {} + ctx.nr.agent.setState = function () {} - t.test('should have gotten an informative error message', (t) => { - collectorApi._login(function test(error) { - t.equal(error.message, 'No agent run ID received from handshake.') + ctx.nr.collectorApi = new CollectorApi(ctx.nr.agent) + }) - redirection.done() - connection.done() + t.afterEach(afterEach) - t.end() + await t.test('should have gotten an error', (t, end) => { + const { collectorApi } = t.nr + collectorApi._login((error) => { + assert.equal(error.message, 'No agent run ID received from handshake.') + end() }) }) - t.test('should pass along no server-side configuration from collector', (t) => { - collectorApi._login(function test(error, res) { - const ssc = res.payload - t.notOk(ssc) - - redirection.done() - connection.done() - - t.end() + await t.test('should pass along no server-side configuration from collector', (t, end) => { + const { collectorApi } = t.nr + collectorApi._login((error, res) => { + assert.equal(res.payload, undefined) + end() }) }) }) -tap.test('receiving 503 response from connect', (t) => { - t.autoend() - - let agent = null - let collectorApi = null - - let redirection = null - let connection = null - - t.beforeEach(() => { - agent = setupMockedAgent() - collectorApi = new CollectorApi(agent) - - nock.disableNetConnect() - - redirection = nock(URL + ':8080') - .post(helper.generateCollectorPath('preconnect')) - .reply(200, { return_value: { redirect_host: HOST, security_policies: {} } }) +test('receiving 503 response from connect', async (t) => { + t.beforeEach(async (ctx) => { + ctx.nr = {} - connection = nock(URL).post(helper.generateCollectorPath('connect')).reply(503) - }) + const collector = new Collector({ runId: RUN_ID }) + ctx.nr.collector = collector + await collector.listen() - t.afterEach(() => { - if (!nock.isDone()) { - /* eslint-disable no-console */ - console.error('Cleaning pending mocks: %j', nock.pendingMocks()) - /* eslint-enable no-console */ - nock.cleanAll() - } + collector.addHandler(helper.generateCollectorPath('connect'), (req, res) => { + res.writeHead(503) + res.end() + }) - nock.enableNetConnect() + const config = Object.assign({}, baseAgentConfig, collector.agentConfig, { + config: { run_id: RUN_ID } + }) + ctx.nr.agent = helper.loadMockedAgent(config) + ctx.nr.agent.reconfigure = function () {} + ctx.nr.agent.setState = function () {} - helper.unloadAgent(agent) - agent = null - collectorApi = null + ctx.nr.collectorApi = new CollectorApi(ctx.nr.agent) }) - t.test('should not have gotten an error', (t) => { - collectorApi._login(function test(error) { - t.error(error) - - redirection.done() - connection.done() + t.afterEach(afterEach) - t.end() + await t.test('should not have gotten an error', (t, end) => { + const { collectorApi } = t.nr + collectorApi._login((error) => { + assert.equal(error, undefined) + end() }) }) - t.test('should have passed on the status code', (t) => { - collectorApi._login(function test(error, response) { - t.error(error) - redirection.done() - connection.done() - - t.equal(response.status, 503) - - t.end() + await t.test('should have passed on the status code', (t, end) => { + const { collectorApi } = t.nr + collectorApi._login((error, res) => { + assert.equal(error, undefined) + assert.equal(res.status, 503) + end() }) }) }) -tap.test('receiving 200 response to connect but no data', (t) => { - t.autoend() - - let agent = null - let collectorApi = null - - let redirection = null - let connection = null - - t.beforeEach(() => { - agent = setupMockedAgent() - collectorApi = new CollectorApi(agent) - - nock.disableNetConnect() +test('receiving 200 response to connect but no data', async (t) => { + t.beforeEach(async (ctx) => { + ctx.nr = {} - redirection = nock(URL + ':8080') - .post(helper.generateCollectorPath('preconnect')) - .reply(200, { return_value: { redirect_host: HOST, security_policies: {} } }) + const collector = new Collector({ runId: RUN_ID }) + ctx.nr.collector = collector + await collector.listen() - connection = nock(URL).post(helper.generateCollectorPath('connect')).reply(200) - }) - - t.afterEach(() => { - if (!nock.isDone()) { - /* eslint-disable no-console */ - console.error('Cleaning pending mocks: %j', nock.pendingMocks()) - /* eslint-enable no-console */ - nock.cleanAll() - } + collector.addHandler(helper.generateCollectorPath('connect'), (req, res) => { + res.writeHead(200) + res.end() + }) - nock.enableNetConnect() + const config = Object.assign({}, baseAgentConfig, collector.agentConfig, { + config: { run_id: RUN_ID } + }) + ctx.nr.agent = helper.loadMockedAgent(config) + ctx.nr.agent.reconfigure = function () {} + ctx.nr.agent.setState = function () {} - helper.unloadAgent(agent) - agent = null - collectorApi = null + ctx.nr.collectorApi = new CollectorApi(ctx.nr.agent) }) - t.test('should have gotten an error', (t) => { - collectorApi._login(function test(error) { - t.ok(error) - - redirection.done() - connection.done() + t.afterEach(afterEach) - t.end() + await t.test('should have gotten an error', (t, end) => { + const { collectorApi } = t.nr + collectorApi._login((error) => { + assert.equal(error.message, 'No agent run ID received from handshake.') + end() }) }) +}) - t.test('should have gotten an informative error message', (t) => { - collectorApi._login(function test(error) { - t.equal(error.message, 'No agent run ID received from handshake.') +async function beforeEach(ctx) { + ctx.nr = {} - redirection.done() - connection.done() + const collector = new Collector({ runId: RUN_ID }) + ctx.nr.collector = collector + await collector.listen() - t.end() - }) + const config = Object.assign({}, baseAgentConfig, collector.agentConfig, { + config: { run_id: RUN_ID } }) -}) + ctx.nr.agent = helper.loadMockedAgent(config) + ctx.nr.agent.reconfigure = function () {} + ctx.nr.agent.setState = function () {} + + ctx.nr.collectorApi = new CollectorApi(ctx.nr.agent) +} -function setupMockedAgent() { - const agent = helper.loadMockedAgent({ - host: HOST, - port: PORT, - app_name: ['TEST'], - license_key: 'license key here', - utilization: { - detect_aws: false, - detect_pcf: false, - detect_azure: false, - detect_gcp: false, - detect_docker: false - }, - browser_monitoring: {}, - transaction_tracer: {} - }) - agent.reconfigure = function () {} - agent.setState = function () {} - - return agent +function afterEach(ctx) { + helper.unloadAgent(ctx.nr.agent) + ctx.nr.collector.close() } diff --git a/test/unit/collector/api-run-lifecycle.test.js b/test/unit/collector/api-run-lifecycle.test.js index 4ab9a23afc..cf0d582c42 100644 --- a/test/unit/collector/api-run-lifecycle.test.js +++ b/test/unit/collector/api-run-lifecycle.test.js @@ -1,328 +1,259 @@ /* - * 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 tap = require('tap') -const nock = require('nock') +const test = require('node:test') +const assert = require('node:assert') +const promiseResolvers = require('../../lib/promise-resolvers') +const Collector = require('../../lib/test-collector') const helper = require('../../lib/agent_helper') const CollectorApi = require('../../../lib/collector/api') -const HOST = 'collector.newrelic.com' -const PORT = 443 -const URL = 'https://' + HOST const RUN_ID = 1337 +const baseAgentConfig = { + app_name: ['TEST'], + ssl: true, + license_key: 'license key here', + utilization: { + detect_aws: false, + detect_pcf: false, + detect_azure: false, + detect_gcp: false, + detect_docker: false + }, + browser_monitoring: {}, + transaction_tracer: {} +} -tap.test('should bail out if disconnected', (t) => { - const agent = setupMockedAgent() - const collectorApi = new CollectorApi(agent) +test('should bail out if disconnected', async (t) => { + await beforeEach(t) + t.after(() => afterEach(t)) - t.teardown(() => { - helper.unloadAgent(agent) + const { collectorApi } = t.nr + const { promise, resolve } = promiseResolvers() + collectorApi._runLifecycle(collectorApi._methods.metric_data, null, (error) => { + assert.equal(error.message, 'Not connected to collector.') + resolve() }) - function tested(error) { - t.ok(error) - t.equal(error.message, 'Not connected to collector.') - - t.end() - } - - const method = collectorApi._methods.metric_data - collectorApi._runLifecycle(method, null, tested) + await promise }) -tap.test('should discard HTTP 413 errors', (t) => { - const agent = setupMockedAgent() - agent.config.run_id = RUN_ID - const collectorApi = new CollectorApi(agent) - - nock.disableNetConnect() +test('should discard HTTP 413 errors', async (t) => { + await beforeEach(t) + t.after(() => afterEach(t)) - t.teardown(() => { - if (!nock.isDone()) { - /* eslint-disable no-console */ - console.error('Cleaning pending mocks: %j', nock.pendingMocks()) - /* eslint-enable no-console */ - nock.cleanAll() - } - - nock.enableNetConnect() - helper.unloadAgent(agent) + const { agent, collector, collectorApi } = t.nr + const { promise, resolve } = promiseResolvers() + collector.addHandler(helper.generateCollectorPath('metric_data', RUN_ID), (req, res) => { + res.writeHead(413) + res.end() }) - - const failure = nock(URL).post(helper.generateCollectorPath('metric_data', RUN_ID)).reply(413) - - function tested(error, command) { - t.error(error) - t.equal(command.retainData, false) - - failure.done() - - t.end() - } - - const method = collectorApi._methods.metric_data - collectorApi._runLifecycle(method, null, tested) -}) - -tap.test('should discard HTTP 415 errors', (t) => { - const agent = setupMockedAgent() agent.config.run_id = RUN_ID - const collectorApi = new CollectorApi(agent) - - nock.disableNetConnect() - - t.teardown(() => { - if (!nock.isDone()) { - /* eslint-disable no-console */ - console.error('Cleaning pending mocks: %j', nock.pendingMocks()) - /* eslint-enable no-console */ - nock.cleanAll() - } - - nock.enableNetConnect() - helper.unloadAgent(agent) + collectorApi._runLifecycle(collectorApi._methods.metric_data, null, (error, cmd) => { + assert.equal(error, undefined) + assert.equal(cmd.retainData, false) + assert.equal(collector.isDone('metric_data'), true) + resolve() }) - const failure = nock(URL).post(helper.generateCollectorPath('metric_data', RUN_ID)).reply(415) - const method = collectorApi._methods.metric_data - collectorApi._runLifecycle(method, null, function tested(error, command) { - t.error(error) - t.equal(command.retainData, false) + await promise +}) - failure.done() +test('should discard HTTP 415 errors', async (t) => { + await beforeEach(t) + t.after(() => afterEach(t)) - t.end() + const { agent, collector, collectorApi } = t.nr + const { promise, resolve } = promiseResolvers() + collector.addHandler(helper.generateCollectorPath('metric_data', RUN_ID), (req, res) => { + res.writeHead(415) + res.end() }) -}) - -tap.test('should retain after HTTP 500 errors', (t) => { - const agent = setupMockedAgent() agent.config.run_id = RUN_ID - const collectorApi = new CollectorApi(agent) - - nock.disableNetConnect() - - t.teardown(() => { - if (!nock.isDone()) { - /* eslint-disable no-console */ - console.error('Cleaning pending mocks: %j', nock.pendingMocks()) - /* eslint-enable no-console */ - nock.cleanAll() - } - - nock.enableNetConnect() - helper.unloadAgent(agent) + collectorApi._runLifecycle(collectorApi._methods.metric_data, null, (error, cmd) => { + assert.equal(error, undefined) + assert.equal(cmd.retainData, false) + assert.equal(collector.isDone('metric_data'), true) + resolve() }) - const failure = nock(URL).post(helper.generateCollectorPath('metric_data', RUN_ID)).reply(500) - - function tested(error, command) { - t.error(error) - t.equal(command.retainData, true) - - failure.done() - - t.end() - } - - const method = collectorApi._methods.metric_data - collectorApi._runLifecycle(method, null, tested) + await promise }) -tap.test('should retain after HTTP 503 errors', (t) => { - const agent = setupMockedAgent() - agent.config.run_id = RUN_ID - const collectorApi = new CollectorApi(agent) - - nock.disableNetConnect() - - t.teardown(() => { - if (!nock.isDone()) { - /* eslint-disable no-console */ - console.error('Cleaning pending mocks: %j', nock.pendingMocks()) - /* eslint-enable no-console */ - nock.cleanAll() - } +test('should retain after HTTP 500 errors', async (t) => { + await beforeEach(t) + t.after(() => afterEach(t)) - nock.enableNetConnect() - helper.unloadAgent(agent) + const { agent, collector, collectorApi } = t.nr + const { promise, resolve } = promiseResolvers() + collector.addHandler(helper.generateCollectorPath('metric_data', RUN_ID), (req, res) => { + res.writeHead(500) + res.end() + }) + agent.config.run_id = RUN_ID + collectorApi._runLifecycle(collectorApi._methods.metric_data, null, (error, cmd) => { + assert.equal(error, undefined) + assert.equal(cmd.retainData, true) + assert.equal(collector.isDone('metric_data'), true) + resolve() }) - const failure = nock(URL).post(helper.generateCollectorPath('metric_data', RUN_ID)).reply(503) - const method = collectorApi._methods.metric_data - collectorApi._runLifecycle(method, null, function tested(error, command) { - t.error(error) - t.equal(command.retainData, true) + await promise +}) - failure.done() +test('should retain after HTTP 503 errors', async (t) => { + await beforeEach(t) + t.after(() => afterEach(t)) - t.end() + const { agent, collector, collectorApi } = t.nr + const { promise, resolve } = promiseResolvers() + collector.addHandler(helper.generateCollectorPath('metric_data', RUN_ID), (req, res) => { + res.writeHead(503) + res.end() }) -}) - -tap.test('should indicate a restart and discard data after 401 errors', (t) => { - const agent = setupMockedAgent() agent.config.run_id = RUN_ID - const collectorApi = new CollectorApi(agent) - - nock.disableNetConnect() - - t.teardown(() => { - if (!nock.isDone()) { - /* eslint-disable no-console */ - console.error('Cleaning pending mocks: %j', nock.pendingMocks()) - /* eslint-enable no-console */ - nock.cleanAll() - } - - nock.enableNetConnect() - helper.unloadAgent(agent) + collectorApi._runLifecycle(collectorApi._methods.metric_data, null, (error, cmd) => { + assert.equal(error, undefined) + assert.equal(cmd.retainData, true) + assert.equal(collector.isDone('metric_data'), true) + resolve() }) - const failure = nock(URL).post(helper.generateCollectorPath('metric_data', RUN_ID)).reply(401) - - function tested(error, command) { - t.error(error) - t.equal(command.retainData, false) - t.equal(command.shouldRestartRun(), true) - - failure.done() - - t.end() - } - - const method = collectorApi._methods.metric_data - collectorApi._runLifecycle(method, null, tested) + await promise }) -tap.test('should indicate a restart and discard data after 409 errors', (t) => { - const agent = setupMockedAgent() - agent.config.run_id = RUN_ID - const collectorApi = new CollectorApi(agent) - - nock.disableNetConnect() +test('should indicate a restart and discard data after 401 errors', async (t) => { + await beforeEach(t) + t.after(() => afterEach(t)) - t.teardown(() => { - if (!nock.isDone()) { - /* eslint-disable no-console */ - console.error('Cleaning pending mocks: %j', nock.pendingMocks()) - /* eslint-enable no-console */ - nock.cleanAll() - } - - nock.enableNetConnect() - helper.unloadAgent(agent) + const { agent, collector, collectorApi } = t.nr + const { promise, resolve } = promiseResolvers() + collector.addHandler(helper.generateCollectorPath('metric_data', RUN_ID), (req, res) => { + res.writeHead(401) + res.end() + }) + agent.config.run_id = RUN_ID + collectorApi._runLifecycle(collectorApi._methods.metric_data, null, (error, cmd) => { + assert.equal(error, undefined) + assert.equal(cmd.retainData, false) + assert.equal(cmd.shouldRestartRun(), true) + assert.equal(collector.isDone('metric_data'), true) + resolve() }) - const failure = nock(URL).post(helper.generateCollectorPath('metric_data', RUN_ID)).reply(409) - const method = collectorApi._methods.metric_data - collectorApi._runLifecycle(method, null, function tested(error, command) { - t.error(error) - t.equal(command.retainData, false) - t.equal(command.shouldRestartRun(), true) + await promise +}) - failure.done() +test('should indicate a restart and discard data after 409 errors', async (t) => { + await beforeEach(t) + t.after(() => afterEach(t)) - t.end() + const { agent, collector, collectorApi } = t.nr + const { promise, resolve } = promiseResolvers() + collector.addHandler(helper.generateCollectorPath('metric_data', RUN_ID), (req, res) => { + res.writeHead(409) + res.end() }) -}) - -tap.test('should stop the agent on 410 (force disconnect)', (t) => { - const agent = setupMockedAgent() agent.config.run_id = RUN_ID - const collectorApi = new CollectorApi(agent) - - nock.disableNetConnect() - - t.teardown(() => { - if (!nock.isDone()) { - /* eslint-disable no-console */ - console.error('Cleaning pending mocks: %j', nock.pendingMocks()) - /* eslint-enable no-console */ - nock.cleanAll() - } - - nock.enableNetConnect() - helper.unloadAgent(agent) + collectorApi._runLifecycle(collectorApi._methods.metric_data, null, (error, cmd) => { + assert.equal(error, undefined) + assert.equal(cmd.retainData, false) + assert.equal(cmd.shouldRestartRun(), true) + assert.equal(collector.isDone('metric_data'), true) + resolve() }) - const shutdownEndpoint = nock(URL) - .post(helper.generateCollectorPath('shutdown', RUN_ID)) - .reply(200, { return_value: null }) - - const failure = nock(URL).post(helper.generateCollectorPath('metric_data', RUN_ID)).reply(410) - - function tested(error, command) { - t.error(error) - t.equal(command.shouldShutdownRun(), true) - - t.notOk(agent.config.run_id) + await promise +}) - failure.done() - shutdownEndpoint.done() +test('should stop the agent on 410 (force disconnect)', async (t) => { + await beforeEach(t) + t.after(() => afterEach(t)) - t.end() - } + const { agent, collector, collectorApi } = t.nr + const { promise, resolve } = promiseResolvers() + collector.addHandler(helper.generateCollectorPath('shutdown', RUN_ID), (req, res) => { + res.json({ payload: { return_value: null } }) + }) + collector.addHandler(helper.generateCollectorPath('metric_data', RUN_ID), (req, res) => { + res.writeHead(410) + res.end() + }) + agent.config.run_id = RUN_ID + collectorApi._runLifecycle(collectorApi._methods.metric_data, null, (error, cmd) => { + assert.equal(error, undefined) + assert.equal(cmd.shouldShutdownRun(), true) + assert.equal(collector.isDone('metric_data'), true) + assert.equal(collector.isDone('shutdown'), true) + assert.equal(agent.config.run_id, null) + resolve() + }) - const method = collectorApi._methods.metric_data - collectorApi._runLifecycle(method, null, tested) + await promise }) -tap.test('should discard unexpected HTTP errors (501)', (t) => { - const agent = setupMockedAgent() +test('should discard unexpected HTTP errors (501)', async (t) => { + await beforeEach(t) + t.after(() => afterEach(t)) + + const { agent, collector, collectorApi } = t.nr + const { promise, resolve } = promiseResolvers() + collector.addHandler(helper.generateCollectorPath('metric_data', RUN_ID), (req, res) => { + res.writeHead(501) + res.end() + }) agent.config.run_id = RUN_ID - const collectorApi = new CollectorApi(agent) + collectorApi._runLifecycle(collectorApi._methods.metric_data, null, (error, cmd) => { + assert.equal(error, undefined) + assert.equal(cmd.retainData, false) + resolve() + }) - nock.disableNetConnect() + await promise +}) - t.teardown(() => { - if (!nock.isDone()) { - /* eslint-disable no-console */ - console.error('Cleaning pending mocks: %j', nock.pendingMocks()) - /* eslint-enable no-console */ - nock.cleanAll() - } +test('should handle error in invoked method', async (t) => { + await beforeEach(t) + t.after(() => afterEach(t)) - nock.enableNetConnect() - helper.unloadAgent(agent) + const { agent, collector, collectorApi } = t.nr + const { promise, resolve } = promiseResolvers() + collector.addHandler(helper.generateCollectorPath('metric_data', RUN_ID), (req) => { + req.destroy() + }) + agent.config.run_id = RUN_ID + collectorApi._runLifecycle(collectorApi._methods.metric_data, null, (error) => { + assert.equal(error.message, 'socket hang up') + assert.equal(error.code, 'ECONNRESET') + resolve() }) - const failure = nock(URL).post(helper.generateCollectorPath('metric_data', RUN_ID)).reply(501) - const method = collectorApi._methods.metric_data - collectorApi._runLifecycle(method, null, function tested(error, command) { - t.error(error) - t.equal(command.retainData, false) + await promise +}) + +async function beforeEach(ctx) { + ctx.nr = {} - failure.done() + const collector = new Collector({ runId: RUN_ID }) + ctx.nr.collector = collector + await collector.listen() - t.end() + const config = Object.assign({}, baseAgentConfig, collector.agentConfig, { + config: { run_id: RUN_ID } }) -}) + ctx.nr.agent = helper.loadMockedAgent(config) + ctx.nr.agent.reconfigure = function () {} + ctx.nr.agent.setState = function () {} -function setupMockedAgent() { - const agent = helper.loadMockedAgent({ - host: HOST, - port: PORT, - app_name: ['TEST'], - ssl: true, - license_key: 'license key here', - utilization: { - detect_aws: false, - detect_pcf: false, - detect_azure: false, - detect_gcp: false, - detect_docker: false - }, - browser_monitoring: {}, - transaction_tracer: {} - }) - agent.reconfigure = function () {} - agent.setState = function () {} + ctx.nr.collectorApi = new CollectorApi(ctx.nr.agent) +} - return agent +function afterEach(ctx) { + helper.unloadAgent(ctx.nr.agent) + ctx.nr.collector.close() } diff --git a/test/unit/collector/api.test.js b/test/unit/collector/api.test.js index 60b9750f3a..a68ef548d7 100644 --- a/test/unit/collector/api.test.js +++ b/test/unit/collector/api.test.js @@ -1,84 +1,57 @@ /* - * 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 tap = require('tap') +const test = require('node:test') +const assert = require('node:assert') +const crypto = require('node:crypto') -const nock = require('nock') -const crypto = require('crypto') +const Collector = require('../../lib/test-collector') const helper = require('../../lib/agent_helper') const CollectorApi = require('../../../lib/collector/api') -const HOST = 'collector.newrelic.com' -const PORT = 443 -const URL = 'https://' + HOST const RUN_ID = 1337 -tap.test('reportSettings', (t) => { - t.autoend() +test('reportSettings', async (t) => { + t.beforeEach(async (ctx) => { + ctx.nr = {} - let agent = null - let collectorApi = null + const collector = new Collector() + ctx.nr.collector = collector + await collector.listen() - let settings = null + const config = Object.assign({}, collector.agentConfig, { config: { run_id: RUN_ID } }) + ctx.nr.agent = helper.loadMockedAgent(config) - const emptySettingsPayload = { - return_value: [] - } - - t.beforeEach(() => { - agent = setupMockedAgent() - agent.config.run_id = RUN_ID - collectorApi = new CollectorApi(agent) - - nock.disableNetConnect() - - settings = nock(URL) - .post(helper.generateCollectorPath('agent_settings', RUN_ID)) - .reply(200, emptySettingsPayload) + ctx.nr.collectorApi = new CollectorApi(ctx.nr.agent) }) - t.afterEach(() => { - if (!nock.isDone()) { - /* eslint-disable no-console */ - console.error('Cleaning pending mocks: %j', nock.pendingMocks()) - /* eslint-enable no-console */ - nock.cleanAll() - } - - nock.enableNetConnect() - - helper.unloadAgent(agent) - agent = null - collectorApi = null + t.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) + ctx.nr.collector.close() }) - t.test('should not error out', (t) => { + await t.test('should not error out', (t, end) => { + const { collectorApi } = t.nr collectorApi.reportSettings((error) => { - t.error(error) - - settings.done() - - t.end() + assert.equal(error, undefined) + end() }) }) - t.test('should return the expected `empty` response', (t) => { + await t.test('should return the expected `empty` response', (t, end) => { + const { collectorApi } = t.nr collectorApi.reportSettings((error, res) => { - t.same(res.payload, emptySettingsPayload.return_value) - - settings.done() - - t.end() + assert.deepStrictEqual(res.payload, []) + end() }) }) - t.test('handles excessive payload sizes without blocking subsequent sends', (t) => { - // remove the nock to agent_settings from beforeEach to avoid a console.error on afterEach - nock.cleanAll() + await t.test('handles excessive payload sizes without blocking subsequent sends', (t, end) => { + const { agent } = t.nr const tstamp = 1_707_756_300_000 // 2024-02-12T11:45:00.000-05:00 function log(data) { return JSON.stringify({ @@ -95,16 +68,13 @@ tap.test('reportSettings', (t) => { const toFind = log('find me') let sends = 0 - const ncontext = nock(URL) - .post(helper.generateCollectorPath('log_event_data', RUN_ID)) - .times(2) - .reply(200) - agent.logs.on('finished log_event_data data send.', () => { sends += 1 if (sends === 3) { - t.equal(ncontext.isDone(), true) - t.end() + const logs = agent.logs.events.toArray() + const found = logs.find((l) => /find me/.test(l)) + assert.notEqual(found, undefined) + end() } }) @@ -117,6 +87,79 @@ tap.test('reportSettings', (t) => { }) }) +test('shutdown', async (t) => { + t.beforeEach(async (ctx) => { + ctx.nr = {} + + const collector = new Collector() + ctx.nr.collector = collector + await collector.listen() + collector.addHandler(helper.generateCollectorPath('shutdown', RUN_ID), (req, res) => { + res.writeHead(503) + res.end() + }) + + const config = Object.assign({}, collector.agentConfig, { + app_name: ['TEST'], + utilization: { + detect_aws: false, + detect_pcf: false, + detect_azure: false, + detect_gcp: false, + detect_docker: false + }, + browser_monitoring: {}, + transaction_tracer: {} + }) + ctx.nr.agent = helper.loadMockedAgent(config) + ctx.nr.agent.reconfig = () => {} + ctx.nr.agent.setState = () => {} + + ctx.nr.collectorApi = new CollectorApi(ctx.nr.agent) + }) + + t.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) + ctx.nr.collector.close() + }) + + await t.test('should not error out', (t, end) => { + const { collectorApi } = t.nr + + collectorApi.shutdown((error) => { + assert.equal(error, undefined) + end() + }) + }) + + await t.test('should no longer have agent run id', (t, end) => { + const { agent, collectorApi } = t.nr + + collectorApi.shutdown(() => { + assert.equal(agent.config.run_id, undefined) + end() + }) + }) + + await t.test('should tell the requester to shut down', (t, end) => { + const { collectorApi } = t.nr + + collectorApi.shutdown((error, res) => { + assert.equal(error, undefined) + assert.equal(res.shouldShutdownRun(), true) + end() + }) + }) + + await t.test('throws if no callback provided', (t) => { + try { + t.nr.collectorApi.shutdown() + } catch (error) { + assert.equal(error.message, 'callback is required') + } + }) +}) + /** * This array contains the data necessary to test the individual collector endpoints * you must provide: @@ -272,264 +315,146 @@ const apiMethods = [ ] } ] -apiMethods.forEach(({ key, data }) => { - tap.test(key, (t) => { - t.autoend() - - t.test('requires errors to send', (t) => { - const agent = setupMockedAgent() - const collectorApi = new CollectorApi(agent) - t.teardown(() => { - helper.unloadAgent(agent) - }) +test('api methods', async (t) => { + t.beforeEach(async (ctx) => { + ctx.nr = {} + + const collector = new Collector() + ctx.nr.collector = collector + await collector.listen() + + const config = Object.assign({}, collector.agentConfig, { + app_name: ['TEST'], + utilization: { + detect_aws: false, + detect_pcf: false, + detect_azure: false, + detect_gcp: false, + detect_docker: false + }, + browser_monitoring: {}, + transaction_tracer: {} + }) + ctx.nr.agent = helper.loadMockedAgent(config) + ctx.nr.agent.reconfigure = () => {} + ctx.nr.agent.setState = () => {} + ctx.nr.agent.config.run_id = RUN_ID - collectorApi.send(key, null, (err) => { - t.ok(err) - t.equal(err.message, `must pass data for ${key} to send`) + ctx.nr.collectorApi = new CollectorApi(ctx.nr.agent) + }) - t.end() - }) - }) + t.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) + ctx.nr.collector.close() + }) - t.test('requires a callback', (t) => { - const agent = setupMockedAgent() - const collectorApi = new CollectorApi(agent) + for (const method of apiMethods) { + await t.test(`${method.key}: requires errors to send`, (t, end) => { + const { collectorApi } = t.nr - t.teardown(() => { - helper.unloadAgent(agent) + collectorApi.send(method.key, null, (error) => { + assert.equal(error.message, `must pass data for ${method.key} to send`) + end() }) - - t.throws(() => { - collectorApi.send(key, [], null) - }, new Error('callback is required')) - t.end() }) - t.test('receiving 200 response, with valid data', (t) => { - t.autoend() + await t.test(`${method.key}: requires a callback`, (t) => { + const { collectorApi } = t.nr - let agent = null - let collectorApi = null - - let dataEndpoint = null - - t.beforeEach(() => { - agent = setupMockedAgent() - agent.config.run_id = RUN_ID - collectorApi = new CollectorApi(agent) - - nock.disableNetConnect() - - const response = { return_value: [] } + assert.throws( + () => { + collectorApi.send(method.key, [], null) + }, + { message: 'callback is required' } + ) + }) - dataEndpoint = nock(URL) - .post(helper.generateCollectorPath(key, RUN_ID)) - .reply(200, response) - }) + await t.test(`${method.key}: should receive 200 without error`, (t, end) => { + const { collector, collectorApi } = t.nr + collector.addHandler(helper.generateCollectorPath(method.key, RUN_ID), async (req, res) => { + const body = await req.body() + const found = JSON.parse(body) - t.afterEach(() => { - if (!nock.isDone()) { - /* eslint-disable no-console */ - console.error('Cleaning pending mocks: %j', nock.pendingMocks()) - /* eslint-enable no-console */ - nock.cleanAll() + let expected = method.data + if (method.data.toJSON) { + expected = method.data.toJSON() } + assert.deepStrictEqual(found, expected) - nock.enableNetConnect() - - helper.unloadAgent(agent) - agent = null - collectorApi = null + res.json({ payload: { return_value: [] } }) }) - - t.test('should not error out', (t) => { - collectorApi.send(key, data, (error) => { - t.error(error) - - dataEndpoint.done() - - t.end() - }) + collectorApi.send(method.key, method.data, (error) => { + assert.equal(error, undefined) + end() }) + }) - t.test('should return retain state', (t) => { - collectorApi.send(key, data, (error, res) => { - t.error(error) - const command = res - - t.equal(command.retainData, false) - - dataEndpoint.done() - - t.end() - }) + await t.test(`${method.key}: should retain state for 200 responses`, (t, end) => { + const { collector, collectorApi } = t.nr + collector.addHandler( + helper.generateCollectorPath(method.key, RUN_ID), + collector.agentSettingsHandler + ) + collectorApi.send(method.key, method.data, (error, res) => { + assert.equal(error, undefined) + assert.equal(res.retainData, false) + end() }) }) - }) + } }) -tap.test('shutdown', (t) => { - t.autoend() - - t.test('requires a callback', (t) => { - const agent = setupMockedAgent() - const collectorApi = new CollectorApi(agent) - - t.teardown(() => { - helper.unloadAgent(agent) +test('send', async (t) => { + t.beforeEach(async (ctx) => { + ctx.nr = {} + + const collector = new Collector() + ctx.nr.collector = collector + await collector.listen() + + const config = Object.assign({}, collector.agentConfig, { + app_name: ['TEST'], + utilization: { + detect_aws: false, + detect_pcf: false, + detect_azure: false, + detect_gcp: false, + detect_docker: false + }, + browser_monitoring: {}, + transaction_tracer: {}, + max_payload_size_in_bytes: 100 }) + ctx.nr.agent = helper.loadMockedAgent(config) + ctx.nr.agent.reconfigure = () => {} + ctx.nr.agent.setState = () => {} + ctx.nr.agent.config.run_id = RUN_ID - t.throws(() => { - collectorApi.shutdown(null) - }, new Error('callback is required')) - - t.end() + ctx.nr.collectorApi = new CollectorApi(ctx.nr.agent) }) - t.test('receiving 200 response, with valid data', (t) => { - t.autoend() - - let agent = null - let collectorApi = null - - let shutdownEndpoint = null - - t.beforeEach(() => { - agent = setupMockedAgent() - agent.config.run_id = RUN_ID - collectorApi = new CollectorApi(agent) - - nock.disableNetConnect() - - const response = { return_value: null } - - shutdownEndpoint = nock(URL) - .post(helper.generateCollectorPath('shutdown', RUN_ID)) - .reply(200, response) - }) - - t.afterEach(() => { - if (!nock.isDone()) { - /* eslint-disable no-console */ - console.error('Cleaning pending mocks: %j', nock.pendingMocks()) - /* eslint-enable no-console */ - nock.cleanAll() - } - - nock.enableNetConnect() - - helper.unloadAgent(agent) - agent = null - collectorApi = null - }) - - t.test('should not error out', (t) => { - collectorApi.shutdown((error) => { - t.error(error) - - shutdownEndpoint.done() - - t.end() - }) - }) - - t.test('should return null', (t) => { - collectorApi.shutdown((error, res) => { - t.equal(res.payload, null) - - shutdownEndpoint.done() - - t.end() - }) - }) + t.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) + ctx.nr.collector.close() }) - t.test('fail on a 503 status code', (t) => { - t.autoend() - - let agent = null - let collectorApi = null - - let shutdownEndpoint = null - - t.beforeEach(() => { - agent = setupMockedAgent() - agent.config.run_id = RUN_ID - collectorApi = new CollectorApi(agent) - - nock.disableNetConnect() - - shutdownEndpoint = nock(URL).post(helper.generateCollectorPath('shutdown', RUN_ID)).reply(503) - }) - - t.afterEach(() => { - if (!nock.isDone()) { - /* eslint-disable no-console */ - console.error('Cleaning pending mocks: %j', nock.pendingMocks()) - /* eslint-enable no-console */ - nock.cleanAll() - } - - nock.enableNetConnect() - - helper.unloadAgent(agent) - agent = null - collectorApi = null - }) - - t.test('should not error out', (t) => { - collectorApi.shutdown((error) => { - t.error(error) - - shutdownEndpoint.done() - - t.end() - }) - }) - - t.test('should no longer have agent run id', (t) => { - collectorApi.shutdown(() => { - t.notOk(agent.config.run_id) - - shutdownEndpoint.done() - - t.end() - }) + await t.test('handles payloads of excessive size', (t, end) => { + const { agent, collector, collectorApi } = t.nr + const data = [ + [ + { type: 'my_custom_typ', timestamp: 1543949274921 }, + { foo: 'a'.repeat(agent.config.max_payload_size_in_bytes + 1) } + ] + ] + collector.addHandler(helper.generateCollectorPath('custom_event_data', RUN_ID), (req, res) => { + res.writeHead(413) + res.end() }) - - t.test('should tell the requester to shut down', (t) => { - collectorApi.shutdown((error, res) => { - const command = res - t.equal(command.shouldShutdownRun(), true) - - shutdownEndpoint.done() - - t.end() - }) + collectorApi.send('custom_event_data', data, (error, result) => { + assert.equal(error, undefined) + assert.deepStrictEqual(result, { retainData: false }) + end() }) }) }) - -function setupMockedAgent() { - const agent = helper.loadMockedAgent({ - host: HOST, - port: PORT, - app_name: ['TEST'], - ssl: true, - license_key: 'license key here', - utilization: { - detect_aws: false, - detect_pcf: false, - detect_azure: false, - detect_gcp: false, - detect_docker: false - }, - browser_monitoring: {}, - transaction_tracer: {} - }) - agent.reconfigure = function () {} - agent.setState = function () {} - - return agent -} diff --git a/test/unit/collector/http-agents.test.js b/test/unit/collector/http-agents.test.js index 8867ea3dd9..0594726b8a 100644 --- a/test/unit/collector/http-agents.test.js +++ b/test/unit/collector/http-agents.test.js @@ -1,179 +1,152 @@ /* - * 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 tap = require('tap') -const proxyquire = require('proxyquire') +const test = require('node:test') +const assert = require('node:assert') +const { HttpsProxyAgent } = require('https-proxy-agent') + const PROXY_HOST = 'unique.newrelic.com' const PROXY_PORT = '54532' const PROXY_URL_WITH_PORT = `https://${PROXY_HOST}:${PROXY_PORT}` const PROXY_URL_WITHOUT_PORT = `https://${PROXY_HOST}` +const httpAgentsPath = require.resolve('../../../lib/collector/http-agents') -tap.test('keepAlive agent', (t) => { - t.autoend() - let agent - let moduleName - let keepAliveAgent - - t.beforeEach(() => { - // We do this to avoid the persistent caching of the agent in this module - moduleName = require.resolve('../../../lib/collector/http-agents') - keepAliveAgent = require(moduleName).keepAliveAgent +test('keepAlive agent', async (t) => { + t.beforeEach((ctx) => { + ctx.nr = {} + ctx.nr.keepAliveAgent = require(httpAgentsPath).keepAliveAgent }) + t.afterEach(() => { - agent = null - delete require.cache[moduleName] + delete require.cache[httpAgentsPath] }) - t.test('configured without params', (t) => { - agent = keepAliveAgent() - t.ok(agent, 'should be created successfully') - t.equal(agent.protocol, 'https:', 'should be set to https') - t.equal(agent.keepAlive, true, 'should be keepAlive') - t.end() + await t.test('configured without params', (t) => { + const agent = t.nr.keepAliveAgent() + assert.ok(agent, 'should be created successfully') + assert.equal(agent.protocol, 'https:', 'should be set to https') + assert.equal(agent.keepAlive, true, 'should be keepAlive') }) - t.test('configured with keepAlive set to false', (t) => { - agent = keepAliveAgent({ keepAlive: false }) - t.ok(agent, 'should be created successfully') - t.equal(agent.protocol, 'https:', 'should be set to https') - t.equal(agent.keepAlive, true, 'should override config and be keepAlive') - t.end() + await t.test('configured with keepAlive set to false', (t) => { + const agent = t.nr.keepAliveAgent({ keepAlive: false }) + assert.ok(agent, 'should be created successfully') + assert.equal(agent.protocol, 'https:', 'should be set to https') + assert.equal(agent.keepAlive, true, 'should be keepAlive') }) - t.test('should return singleton instance if called more than once', (t) => { - agent = keepAliveAgent({ keepAlive: false }) - const agent2 = keepAliveAgent() - t.same(agent, agent2) - t.end() + await t.test('should return singleton instance if called more than once', (t) => { + const agent = t.nr.keepAliveAgent({ keepAlive: false }) + const agent2 = t.nr.keepAliveAgent() + assert.equal(agent, agent2) }) }) -tap.test('proxy agent', (t) => { - t.autoend() - let agent - let moduleName - let proxyAgent - - t.beforeEach(() => { - // We do this to avoid the persistent caching of the agent in this module - moduleName = require.resolve('../../../lib/collector/http-agents') - proxyAgent = require(moduleName).proxyAgent + +test('proxy agent', async (t) => { + t.beforeEach((ctx) => { + ctx.nr = {} + ctx.nr.proxyAgent = require(httpAgentsPath).proxyAgent }) + t.afterEach(() => { - agent = null - delete require.cache[moduleName] + delete require.cache[httpAgentsPath] }) - t.test('configured without params', (t) => { - t.throws(() => (agent = proxyAgent()), 'should throw without config') - t.ok(() => (agent = proxyAgent({})), 'should not throw when config has no content') - t.notOk(agent, 'agent should not be created without valid config') - t.end() + await t.test('configured without params', (t) => { + assert.throws(() => t.nr.proxyAgent(), 'should throw without config') }) - t.test('configured with proxy host and proxy port', (t) => { + await t.test('configured with proxy host and proxy port', (t) => { const config = { proxy_host: PROXY_HOST, proxy_port: PROXY_PORT } - agent = proxyAgent(config) - t.ok(agent, 'should be created successfully') - t.equal(agent.proxy.hostname, PROXY_HOST, 'should have correct proxy host') - t.equal(agent.proxy.port, PROXY_PORT, 'should have correct proxy port') - t.equal(agent.proxy.protocol, 'https:', 'should be set to https') - t.equal(agent.keepAlive, true, 'should be keepAlive') - t.end() - }) - - t.test('configured with proxy url:port', (t) => { - const config = { - proxy: PROXY_URL_WITH_PORT - } - agent = proxyAgent(config) - t.ok(agent, 'should be created successfully') - t.equal(agent.proxy.hostname, PROXY_HOST, 'should have correct proxy host') - t.equal(agent.proxy.port, PROXY_PORT, 'should have correct proxy port') - t.equal(agent.proxy.protocol, 'https:', 'should be set to https') - t.equal(agent.keepAlive, true, 'should be keepAlive') - t.end() + const agent = t.nr.proxyAgent(config) + assert.ok(agent, 'should be created successfully') + assert.equal(agent.proxy.hostname, PROXY_HOST, 'should have correct proxy host') + assert.equal(agent.proxy.port, PROXY_PORT, 'should have correct proxy port') + assert.equal(agent.proxy.protocol, 'https:', 'should be set to https') + assert.equal(agent.keepAlive, true, 'should be keepAlive') }) - t.test('should return singleton of proxyAgent if called more than once', (t) => { + await t.test('configured with proxy url:port', (t) => { const config = { proxy: PROXY_URL_WITH_PORT } - agent = proxyAgent(config) - const agent2 = proxyAgent() - t.same(agent, agent2) - t.end() + const agent = t.nr.proxyAgent(config) + assert.ok(agent, 'should be created successfully') + assert.equal(agent.proxy.hostname, PROXY_HOST, 'should have correct proxy host') + assert.equal(agent.proxy.port, PROXY_PORT, 'should have correct proxy port') + assert.equal(agent.proxy.protocol, 'https:', 'should be set to https') + assert.equal(agent.keepAlive, true, 'should be keepAlive') }) - t.test('configured with proxy url only', (t) => { + await t.test('configured with proxy url only', (t) => { const config = { proxy: PROXY_URL_WITHOUT_PORT } - agent = proxyAgent(config) - t.ok(agent, 'should be created successfully') - t.equal(agent.proxy.hostname, PROXY_HOST, 'should have correct proxy host') - t.equal(agent.proxy.protocol, 'https:', 'should be set to https') - t.equal(agent.keepAlive, true, 'should be keepAlive') - t.equal(agent.connectOpts.secureEndpoint, undefined) - t.end() + const agent = t.nr.proxyAgent(config) + assert.ok(agent, 'should be created successfully') + assert.equal(agent.proxy.hostname, PROXY_HOST, 'should have correct proxy host') + assert.equal(agent.proxy.port, '', 'should have correct proxy port') + assert.equal(agent.proxy.protocol, 'https:', 'should be set to https') + assert.equal(agent.keepAlive, true, 'should be keepAlive') + assert.equal(agent.connectOpts.secureEndpoint, undefined) }) - t.test('configured with certificates defined', (t) => { - const { proxyAgent } = proxyquire('../../../lib/collector/http-agents', { - 'https-proxy-agent': { HttpsProxyAgent: Mock } - }) + await t.test('should return singleton of proxyAgent if called more than once', (t) => { + const config = { proxy: PROXY_URL_WITH_PORT } + const agent = t.nr.proxyAgent(config) + const agent2 = t.nr.proxyAgent() + assert.equal(agent, agent2) + }) + await t.test('configured with certificates defined', (t) => { const config = { proxy: PROXY_URL_WITH_PORT, certificates: ['cert1'], ssl: true } - function Mock(host, opts) { - t.equal(host, PROXY_URL_WITH_PORT, 'should have correct proxy url') - t.same(opts.ca, ['cert1'], 'should have correct certs') - t.equal(opts.keepAlive, true, 'should be keepAlive') - t.equal(opts.secureEndpoint, true) - t.end() - } - - proxyAgent(config) + const agent = t.nr.proxyAgent(config) + assert.equal(agent instanceof HttpsProxyAgent, true) + assert.equal(agent.proxy.host, `${PROXY_HOST}:${PROXY_PORT}`, 'should have correct proxy host') + assert.deepStrictEqual(agent.connectOpts.ca, ['cert1'], 'should have correct certs') + assert.equal(agent.connectOpts.keepAlive, true, 'should be keepAlive') + assert.equal(agent.connectOpts.secureEndpoint, true) }) - t.test('should default to localhost if no proxy_host or proxy_port is specified', (t) => { + await t.test('should default to localhost if no proxy_host or proxy_port is specified', (t) => { const config = { proxy_user: 'unit-test', proxy_pass: 'secret', ssl: true } - agent = proxyAgent(config) - t.ok(agent, 'should be created successfully') - t.equal(agent.proxy.hostname, 'localhost', 'should have correct proxy host') - t.equal(agent.proxy.port, '80', 'should have correct proxy port') - t.equal(agent.proxy.protocol, 'https:', 'should be set to https') - t.equal(agent.proxy.username, 'unit-test', 'should have correct basic auth username') - t.equal(agent.proxy.password, 'secret', 'should have correct basic auth password') - t.equal(agent.connectOpts.secureEndpoint, true) - t.end() + const agent = t.nr.proxyAgent(config) + assert.ok(agent, 'should be created successfully') + assert.equal(agent.proxy.hostname, 'localhost', 'should have correct proxy host') + assert.equal(agent.proxy.port, '80', 'should have correct proxy port') + assert.equal(agent.proxy.protocol, 'https:', 'should be set to https') + assert.equal(agent.proxy.username, 'unit-test', 'should have correct basic auth username') + assert.equal(agent.proxy.password, 'secret', 'should have correct basic auth password') + assert.equal(agent.connectOpts.secureEndpoint, true) }) - t.test('should not parse basic auth user if password is empty', (t) => { + await t.test('should not parse basic auth user if password is empty', (t) => { const config = { proxy_user: 'unit-test', - proxy_pass: '' + proxy_pass: '', + ssl: true } - agent = proxyAgent(config) - t.ok(agent, 'should be created successfully') - t.equal(agent.proxy.hostname, 'localhost', 'should have correct proxy host') - t.equal(agent.proxy.port, '80', 'should have correct proxy port') - t.equal(agent.proxy.protocol, 'https:', 'should be set to https') - t.not(agent.proxy.username, 'should not have basic auth username') - t.not(agent.proxy.password, 'should not have basic auth password') - t.end() + const agent = t.nr.proxyAgent(config) + assert.ok(agent, 'should be created successfully') + assert.equal(agent.proxy.hostname, 'localhost', 'should have correct proxy host') + assert.equal(agent.proxy.port, '80', 'should have correct proxy port') + assert.equal(agent.proxy.protocol, 'https:', 'should be set to https') + assert.equal(agent.proxy.username, '', 'should not have basic auth username') + assert.equal(agent.proxy.password, '', 'should not have basic auth password') }) }) diff --git a/test/unit/collector/key-parser.test.js b/test/unit/collector/key-parser.test.js index 8ee01eab2b..50987f4c1c 100644 --- a/test/unit/collector/key-parser.test.js +++ b/test/unit/collector/key-parser.test.js @@ -1,27 +1,24 @@ /* - * 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 tap = require('tap') +const test = require('node:test') +const assert = require('node:assert') const parse = require('../../../lib/collector/key-parser').parseKey -tap.test('collector license key parser', (t) => { - t.test('should return the region prefix when a region is detected', (t) => { +test('collector license key parser', async (t) => { + await t.test('should return the region prefix when a region is detected', () => { const testKey = 'eu01xx66c637a29c3982469a3fe8d1982d002c4a' const region = parse(testKey) - t.equal(region, 'eu01') - t.end() + assert.equal(region, 'eu01') }) - t.test('should return null when a region is not detected', (t) => { + await t.test('should return null when a region is not defined', () => { const testKey = '08a2ad66c637a29c3982469a3fe8d1982d002c4a' const region = parse(testKey) - t.equal(region, null) - t.end() + assert.equal(region, null) }) - - t.end() }) diff --git a/test/unit/collector/parse-response.test.js b/test/unit/collector/parse-response.test.js index ff77a7647e..a4ca94f602 100644 --- a/test/unit/collector/parse-response.test.js +++ b/test/unit/collector/parse-response.test.js @@ -1,181 +1,135 @@ /* - * 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 tap = require('tap') - +const test = require('node:test') +const assert = require('node:assert') const parse = require('../../../lib/collector/parse-response') -tap.test('should call back with an error if called with no collector method name', (t) => { - parse(null, { statusCode: 200 }, (err) => { - t.ok(err) - t.equal(err.message, 'collector method name required!') - - t.end() +test('should call back with an error if called with no collector method name', (t, end) => { + parse(null, { statusCode: 200 }, (error) => { + assert.equal(error.message, 'collector method name required!') + end() }) }) -tap.test('should call back with an error if called without a response', (t) => { - parse('TEST', null, (err) => { - t.ok(err) - t.equal(err.message, 'HTTP response required!') - - t.end() +test('should call back with an error if called without a response', (t, end) => { + parse('TEST', null, (error) => { + assert.equal(error.message, 'HTTP response required!') + end() }) }) -tap.test('should throw if called without a callback', (t) => { - const response = { statusCode: 200 } - t.throws(() => { - parse('TEST', response, undefined) - }, new Error('callback required!')) - - t.end() +test('should throw if called without a callback', () => { + assert.throws(() => { + parse('TEST', { statusCode: 200 }, undefined) + }, /callback required!/) }) -tap.test('when initialized properly and response status is 200', (t) => { - t.autoend() - +test('when initialized properly and response status is 200', async (t) => { const response = { statusCode: 200 } const methodName = 'TEST' - t.test('should pass through return value', (t) => { - function callback(error, res) { - t.same(res.payload, [1, 1, 2, 3, 5, 8]) - - t.end() - } - - const parser = parse(methodName, response, callback) + await t.test('should pass through return value', (t, end) => { + const parser = parse(methodName, response, (error, res) => { + assert.deepStrictEqual(res.payload, [1, 1, 2, 3, 5, 8]) + end() + }) parser(null, '{"return_value":[1,1,2,3,5,8]}') }) - t.test('should pass through status code', (t) => { - function callback(error, res) { - t.equal(res.status, 200) - t.end() - } - - const parser = parse(methodName, response, callback) + await t.test('should pass through status code', (t, end) => { + const parser = parse(methodName, response, (error, res) => { + assert.deepStrictEqual(res.status, 200) + end() + }) parser(null, '{"return_value":[1,1,2,3,5,8]}') }) - t.test('should pass through even a null return value', (t) => { - function callback(error, res) { - t.equal(res.payload, null) - t.end() - } - - const parser = parse(methodName, response, callback) + await t.test('should pass through even a null return value', (t, end) => { + const parser = parse(methodName, response, (error, res) => { + assert.equal(res.payload, null) + end() + }) parser(null, '{"return_value":null}') }) - t.test('should not error on an explicitly null return value', (t) => { - function callback(error) { - t.error(error) - t.end() - } - - const parser = parse(methodName, response, callback) + await t.test('should not error on an explicitly null return value', (t, end) => { + const parser = parse(methodName, response, (error) => { + assert.equal(error, undefined) + end() + }) parser(null, '{"return_value":null}') }) - t.test('should not error in normal situations', (t) => { - function callback(error) { - t.error(error) - t.end() - } - - const parser = parse(methodName, response, callback) + await t.test('should not error in normal situations', (t, end) => { + const parser = parse(methodName, response, (error) => { + assert.equal(error, undefined) + end() + }) parser(null, '{"return_value":[1,1,2,3,5,8]}') }) - t.test('should not error on a missing body', (t) => { - function callback(error, res) { - t.error(error) - t.equal(res.status, 200) - t.end() - } - - const parser = parse(methodName, response, callback) + await t.test('should not erro on a missing body', (t, end) => { + const parser = parse(methodName, response, (error) => { + assert.equal(error, undefined) + end() + }) parser(null, null) }) - t.test('should not error on unparsable return value', (t) => { - function callback(error, res) { - t.error(error) - - t.notOk(res.payload) - t.equal(res.status, 200) - - t.end() - } - - const exception = 'hi' - - const parser = parse(methodName, response, callback) - parser(null, exception) + await t.test('should not error on unparseable return value', (t, end) => { + const parser = parse(methodName, response, (error, res) => { + assert.equal(error, undefined) + assert.equal(res.payload, undefined) + assert.equal(res.status, 200) + end() + }) + parser(null, 'hi') }) - t.test('should not error on a server exception with no error message', (t) => { - const exception = '{"exception":{"error_type":"RuntimeError"}}' - - const parser = parse(methodName, response, function callback(error, res) { - t.error(error) - - t.notOk(res.payload) - t.equal(res.status, 200) - - t.end() + await t.test('should not error on server exception with no error message', (t, end) => { + const parser = parse(methodName, response, (error, res) => { + assert.equal(error, undefined) + assert.equal(res.payload, undefined) + assert.equal(res.status, 200) + end() }) - parser(null, exception) + parser(null, '{"exception":{"error_type":"RuntimeError"}}') }) - t.test('should pass back passed in errors before missing body errors', (t) => { - function callback(error) { - t.ok(error) - t.equal(error.message, 'oh no!') - - t.end() - } - - const parser = parse(methodName, response, callback) - parser(new Error('oh no!'), null) + await t.test('should pass back passed in errors before missing body errors', (t, end) => { + const parser = parse(methodName, response, (error) => { + assert.equal(error.message, 'oh no!') + end() + }) + parser(Error('oh no!'), null) }) }) -tap.test('when initialized properly and response status is 503', (t) => { - t.autoend() - +test('when initialized properly and response status is 503', async (t) => { const response = { statusCode: 503 } const methodName = 'TEST' - t.test('should pass through return value despite weird status code', (t) => { - function callback(error, res) { - t.error(error) - - t.same(res.payload, [1, 1, 2, 3, 5, 8]) - t.equal(res.status, 503) - - t.end() - } - - const parser = parse(methodName, response, callback) + await t.test('should pass through return value despite weird status code', (t, end) => { + const parser = parse(methodName, response, (error, res) => { + assert.equal(error, undefined) + assert.deepStrictEqual(res.payload, [1, 1, 2, 3, 5, 8]) + assert.equal(res.status, 503) + end() + }) parser(null, '{"return_value":[1,1,2,3,5,8]}') }) - t.test('should not error on no return value or server exception', (t) => { - function callback(error, res) { - t.error(error) - t.equal(res.status, 503) - - t.end() - } - - const parser = parse(methodName, response, callback) + await t.test('should not error on no return value or server exception', (t, end) => { + const parser = parse(methodName, response, (error, res) => { + assert.equal(error, undefined) + assert.deepStrictEqual(res.status, 503) + end() + }) parser(null, '{}') }) }) diff --git a/test/unit/collector/remote-method.test.js b/test/unit/collector/remote-method.test.js index 0831db5141..1bd1087b1f 100644 --- a/test/unit/collector/remote-method.test.js +++ b/test/unit/collector/remote-method.test.js @@ -1,249 +1,209 @@ /* - * 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 tap = require('tap') -const dns = require('dns') -const events = require('events') -const https = require('https') -const sinon = require('sinon') +const test = require('node:test') +const assert = require('node:assert') +const https = require('node:https') +const events = require('node:events') +const dns = require('node:dns') +const url = require('node:url') const proxyquire = require('proxyquire') -const RemoteMethod = require('../../../lib/collector/remote-method') -const url = require('url') -const Config = require('../../../lib/config') const helper = require('../../lib/agent_helper') -require('../../lib/metrics_helper') +const Config = require('../../../lib/config') +const Collector = require('../../lib/test-collector') +const { assertMetricValues } = require('../../lib/assert-metrics') +const RemoteMethod = require('../../../lib/collector/remote-method') + const NAMES = require('../../../lib/metrics/names') +const RUN_ID = 1337 const BARE_AGENT = { config: {}, metrics: { measureBytes() {} } } -function generate(method, runID, protocolVersion) { - protocolVersion = protocolVersion || 17 - let fragment = - '/agent_listener/invoke_raw_method?' + - `marshal_format=json&protocol_version=${protocolVersion}&` + - `license_key=license%20key%20here&method=${method}` - - if (runID) { - fragment += `&run_id=${runID}` - } - - return fragment -} - -tap.test('should require a name for the method to call', (t) => { - t.throws(() => { - new RemoteMethod() // eslint-disable-line no-new - }) - t.end() +test('should require a name for the method to call', () => { + assert.throws(() => new RemoteMethod()) }) -tap.test('should require an agent for the method to call', (t) => { - t.throws(() => { - new RemoteMethod('test') // eslint-disable-line no-new - }) - t.end() +test('should require an agent for the method to call', () => { + assert.throws(() => new RemoteMethod('test')) }) -tap.test('should expose a call method as its public API', (t) => { - t.type(new RemoteMethod('test', BARE_AGENT).invoke, 'function') - t.end() +test('should expose a call method as its public API', () => { + const method = new RemoteMethod('test', BARE_AGENT) + assert.equal(typeof method.invoke, 'function') }) -tap.test('should expose its name', (t) => { - t.equal(new RemoteMethod('test', BARE_AGENT).name, 'test') - t.end() +test('should expose its name', () => { + const method = new RemoteMethod('test', BARE_AGENT) + assert.equal(method.name, 'test') }) -tap.test('should default to protocol 17', (t) => { - t.equal(new RemoteMethod('test', BARE_AGENT)._protocolVersion, 17) - t.end() +test('should default to protocol 17', () => { + const method = new RemoteMethod('test', BARE_AGENT) + assert.equal(method._protocolVersion, 17) }) -tap.test('serialize', (t) => { - t.autoend() - - let method = null - - t.beforeEach(() => { - method = new RemoteMethod('test', BARE_AGENT) +test('serialize', async (t) => { + t.beforeEach((ctx) => { + ctx.nr = {} + ctx.nr.method = new RemoteMethod('test', BARE_AGENT) }) - t.test('should JSON-encode the given payload', (t) => { - method.serialize({ foo: 'bar' }, (err, encoded) => { - t.error(err) - - t.equal(encoded, '{"foo":"bar"}') - t.end() + await t.test('should JSON-encode the given payload', (t, end) => { + const { method } = t.nr + method.serialize({ foo: 'bar' }, (error, encoded) => { + assert.equal(error, undefined) + assert.equal(encoded, '{"foo":"bar"}') + end() }) }) - t.test('should not error with circular payloads', (t) => { + await t.test('should not error with circular payloads', (t, end) => { + const { method } = t.nr const obj = { foo: 'bar' } obj.obj = obj - method.serialize(obj, (err, encoded) => { - t.error(err) - - t.equal(encoded, '{"foo":"bar","obj":"[Circular ~]"}') - t.end() + method.serialize(obj, (error, encoded) => { + assert.equal(error, undefined) + assert.equal(encoded, '{"foo":"bar","obj":"[Circular ~]"}') + end() }) }) - t.test('should be able to handle a bigint', (t) => { - const obj = { big: BigInt('1729') } - method.serialize(obj, (err, encoded) => { - t.error(err) - t.equal(encoded, '{"big":"1729"}') - t.end() + await t.test('should be able to handle a bigint', (t, end) => { + const { method } = t.nr + const obj = { big: 1729n } + method.serialize(obj, (error, encoded) => { + assert.equal(error, undefined) + assert.equal(encoded, '{"big":"1729"}') + end() }) }) - t.test('should catch serialization errors', (t) => { - method.serialize( - { - toJSON: () => { - throw new Error('fake serialization error') - } - }, - (err, encoded) => { - t.ok(err) - t.equal(err.message, 'fake serialization error') - - t.notOk(encoded) - t.end() + await t.test('should catch serialization errors', (t, end) => { + const { method } = t.nr + const obj = { + toJSON() { + throw Error('fake serialization error') } - ) + } + method.serialize(obj, (error, encoded) => { + assert.equal(error.message, 'fake serialization error') + assert.equal(encoded, undefined) + end() + }) }) }) -tap.test('_safeRequest', (t) => { - t.autoend() +test('_safeRequest', async (t) => { + t.beforeEach((ctx) => { + ctx.nr = {} + + ctx.nr.agent = helper.instrumentMockedAgent() + ctx.nr.agent.config = { max_payload_size_in_bytes: 100 } - let method = null - let options = null - let agent = null + ctx.nr.method = new RemoteMethod('test', ctx.nr.agent) - t.beforeEach(() => { - agent = helper.instrumentMockedAgent() - agent.config = { max_payload_size_in_bytes: 100 } - method = new RemoteMethod('test', agent) - options = { + ctx.nr.options = { host: 'collector.newrelic.com', port: 80, - onError: () => {}, - onResponse: () => {}, + onError() {}, + onResponse() {}, body: [], path: '/nonexistent' } }) - t.afterEach(() => { - helper.unloadAgent(agent) + t.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) }) - t.test('requires an options hash', (t) => { - t.throws(() => { - method._safeRequest() - }, new Error('Must include options to make request!')) - t.end() + await t.test('requires an options hash', (t) => { + const { method } = t.nr + assert.throws(() => method._safeRequest(), /Must include options to make request!/) }) - t.test('requires a collector hostname', (t) => { + await t.test('requires a collector hostname', (t) => { + const { method, options } = t.nr delete options.host - t.throws(() => { - method._safeRequest(options) - }, new Error('Must include collector hostname!')) - t.end() + assert.throws(() => method._safeRequest(options), /Must include collector hostname!/) }) - t.test('requires a collector port', (t) => { + await t.test('requires a collector port', (t) => { + const { method, options } = t.nr delete options.port - t.throws(() => { - method._safeRequest(options) - }, new Error('Must include collector port!')) - t.end() + assert.throws(() => method._safeRequest(options), /Must include collector port!/) }) - t.test('requires an error callback', (t) => { + await t.test('requires an error callback', (t) => { + const { method, options } = t.nr delete options.onError - t.throws(() => { - method._safeRequest(options) - }, new Error('Must include error handler!')) - t.end() + assert.throws(() => method._safeRequest(options), /Must include error handler!/) }) - t.test('requires a response callback', (t) => { + await t.test('requires a response callback', (t) => { + const { method, options } = t.nr delete options.onResponse - t.throws(() => { - method._safeRequest(options) - }, new Error('Must include response handler!')) - t.end() + assert.throws(() => method._safeRequest(options), /Must include response handler!/) }) - t.test('requires a request body', (t) => { + await t.test('requires a request body', (t) => { + const { method, options } = t.nr delete options.body - t.throws(() => { - method._safeRequest(options) - }, new Error('Must include body to send to collector!')) - t.end() + assert.throws(() => method._safeRequest(options), /Must include body to send to collector!/) }) - t.test('requires a request URL', (t) => { + await t.test('requires a request URL', (t) => { + const { method, options } = t.nr delete options.path - t.throws(() => { - method._safeRequest(options) - }, new Error('Must include URL to request!')) - t.end() + assert.throws(() => method._safeRequest(options), /Must include URL to request!/) }) - t.test('requires a request body within the maximum payload size limit', (t) => { + await t.test('requires a request body within the maximum payload size limit', (t) => { + const { agent, method, options } = t.nr options.body = 'a'.repeat(method._config.max_payload_size_in_bytes + 1) - t.throws(() => { + + try { method._safeRequest(options) - }, new Error('Maximum payload size exceeded')) + } catch (error) { + assert.equal(error.message, 'Maximum payload size exceeded') + assert.equal(error.code, 'NR_REMOTE_METHOD_MAX_PAYLOAD_SIZE_EXCEEDED') + } + const { unscoped: metrics } = helper.getMetrics(agent) - t.ok( - metrics['Supportability/Nodejs/Collector/MaxPayloadSizeLimit/test'], - 'should log MaxPayloadSizeLimit supportability metric' + assert.equal( + metrics['Supportability/Nodejs/Collector/MaxPayloadSizeLimit/test'].callCount, + 1, + 'should log MaxPayloadSizeLimit supportibility metric' ) - t.end() }) }) -tap.test('when calling a method on the collector', (t) => { - t.autoend() - - t.test('should not throw when dealing with compressed data', (t) => { +test('when calling a method on the collector', async (t) => { + await t.test('should not throw when dealing with compressed data', (t, end) => { const method = new RemoteMethod('test', BARE_AGENT, { host: 'localhost' }) method._shouldCompress = () => true method._safeRequest = (options) => { - t.equal(options.body.readUInt8(0), 31) - t.equal(options.body.length, 26) - - t.end() + assert.equal(options.body.readUInt8(0), 31) + assert.equal(options.body.length, 26) + end() } - method.invoke('data', {}) }) - t.test('should not throw when preparing uncompressed data', (t) => { + await t.test('should not throw when preparing uncompressed data', (t, end) => { const method = new RemoteMethod('test', BARE_AGENT, { host: 'localhost' }) method._safeRequest = (options) => { - t.equal(options.body, '"data"') - - t.end() + assert.equal(options.body, '"data"') + end() } - method.invoke('data', {}) }) }) -tap.test('when the connection fails', (t) => { - t.autoend() - - t.test('should return the connection failure', (t) => { +test('when the connection fails', async (t) => { + await t.test('should return the connection failure', (t, end) => { const req = https.request https.request = () => { const error = Error('no server') @@ -254,616 +214,438 @@ tap.test('when the connection fails', (t) => { } return r } - t.teardown(() => { + t.after(() => { https.request = req }) - const config = { - max_payload_size_in_bytes: 100000 - } - - const endpoint = { - host: 'localhost', - port: 8765 - } - + const config = { max_payload_size_in_bytes: 100_000 } + const endpoint = { host: 'localhost', port: 8765 } const method = new RemoteMethod('TEST', { ...BARE_AGENT, config }, endpoint) method.invoke({ message: 'none' }, {}, (error) => { - t.ok(error) - // regex for either ipv4 or ipv6 localhost - t.equal(error.code, 'ECONNREFUSED') - - t.end() + assert.equal(error.code, 'ECONNREFUSED') + end() }) }) - t.test('should correctly handle a DNS lookup failure', (t) => { + await t.test('should correctly handle a DNS lookup failure', (t, end) => { const lookup = dns.lookup dns.lookup = (a, b, cb) => { const error = Error('no dns') error.code = dns.NOTFOUND return cb(error) } - t.teardown(() => { + t.after(() => { dns.lookup = lookup }) - const config = { - max_payload_size_in_bytes: 100000 - } - const endpoint = { - host: 'failed.domain.cxlrg', - port: 80 - } + const config = { max_payload_size_in_bytes: 100_000 } + const endpoint = { host: 'failed.domain.cxlrg', port: 80 } const method = new RemoteMethod('TEST', { ...BARE_AGENT, config }, endpoint) method.invoke([], {}, (error) => { - t.ok(error) - t.equal(error.message, 'no dns') - t.end() + assert.equal(error.message, 'no dns') + end() }) }) }) -tap.test('when posting to collector', (t) => { - t.autoend() - - const RUN_ID = 1337 - const URL = 'https://collector.newrelic.com' - let nock = null - let config = null - let method = null +test('when posting to collector', async (t) => { + t.beforeEach(async (ctx) => { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' + ctx.nr = {} - t.beforeEach(() => { - // TODO: is this true? - // order dependency: requiring nock at the top of the file breaks other tests - nock = require('nock') - nock.disableNetConnect() + const collector = new Collector({ runId: RUN_ID }) + ctx.nr.collector = collector + await collector.listen() - config = new Config({ + ctx.nr.config = new Config({ ssl: true, run_id: RUN_ID, license_key: 'license key here' }) + ctx.nr.endpoint = { host: collector.host, port: collector.port } - const endpoint = { - host: 'collector.newrelic.com', - port: 443 - } - - method = new RemoteMethod('metric_data', { ...BARE_AGENT, config }, endpoint) + ctx.nr.method = new RemoteMethod( + 'metric_data', + { ...BARE_AGENT, config: ctx.nr.config }, + ctx.nr.endpoint + ) }) - t.afterEach(() => { - config = null - method = null - nock.cleanAll() - nock.enableNetConnect() + t.afterEach((ctx) => { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '1' + ctx.nr.collector.close() }) - t.test('should pass through error when compression fails', (t) => { - method = new RemoteMethod('test', BARE_AGENT, { host: 'localhost' }) + await t.test('should pass through error when compression fails', (t, end) => { + const { method } = t.nr method._shouldCompress = () => true - // zlib.deflate really wants a stringlike entity method._post(-1, {}, (error) => { - t.ok(error) - - t.end() + assert.equal( + error.message.startsWith( + 'The "chunk" argument must be of type string or an instance of Buffer' + ), + true + ) + end() }) }) - t.test('successfully', (t) => { - t.autoend() - - function nockMetricDataUncompressed() { - return nock(URL) - .post(generate('metric_data', RUN_ID)) - .matchHeader('Content-Encoding', 'identity') - .reply(200, { return_value: [] }) - } - - t.test('should invoke the callback without error', (t) => { - nockMetricDataUncompressed() - method._post('[]', {}, (error) => { - t.error(error) - t.end() - }) + await t.test('successfully', async (t) => { + t.beforeEach((ctx) => { + ctx.nr.requestMethod = '' + ctx.nr.headers = {} + ctx.nr.collector.addHandler( + helper.generateCollectorPath('metric_data', RUN_ID), + (req, res) => { + const encoding = req.headers['content-encoding'] + assert.equal(['identity', 'deflate', 'gzip'].includes(encoding), true) + ctx.nr.requestMethod = req.method + ctx.nr.headers = req.headers + res.json({ payload: { return_value: [] } }) + } + ) }) - t.test('should use the right URL', (t) => { - const sendMetrics = nockMetricDataUncompressed() + await t.test('should invoke the callback without error', (t, end) => { + const { collector, method } = t.nr method._post('[]', {}, (error) => { - t.error(error) - t.ok(sendMetrics.isDone()) - t.end() + assert.equal(error, undefined) + assert.equal(collector.isDone('metric_data'), true) + end() }) }) - t.test('should respect the put_for_data_send config', (t) => { - const putMetrics = nock(URL) - .put(generate('metric_data', RUN_ID)) - .reply(200, { return_value: [] }) - - config.put_for_data_send = true - + await t.test('should use the right URL', (t, end) => { + const { collector, method } = t.nr method._post('[]', {}, (error) => { - t.error(error) - t.ok(putMetrics.isDone()) - - t.end() + assert.equal(error, undefined) + assert.equal(collector.isDone('metric_data'), true) + end() }) }) - t.test('should default to gzip compression', (t) => { - const sendGzippedMetrics = nock(URL) - .post(generate('metric_data', RUN_ID)) - .matchHeader('Content-Encoding', 'gzip') - .reply(200, { return_value: [] }) - - method._shouldCompress = () => true + await t.test('should respect the put_for_data_send config', (t, end) => { + const { collector, method } = t.nr + t.nr.config.put_for_data_send = true method._post('[]', {}, (error) => { - t.error(error) - - t.ok(sendGzippedMetrics.isDone()) - - t.end() + assert.equal(error, undefined) + assert.equal(collector.isDone('metric_data'), true) + assert.equal(t.nr.requestMethod, 'PUT') + end() }) }) - t.test('should use deflate compression when requested', (t) => { - method._agent.config.compressed_content_encoding = 'deflate' - const sendDeflatedMetrics = nock(URL) - .post(generate('metric_data', RUN_ID)) - .matchHeader('Content-Encoding', 'deflate') - .reply(200, { return_value: [] }) - + await t.test('should default to gzip compression', (t, end) => { + const { collector, method } = t.nr + t.nr.config.put_for_data_send = true method._shouldCompress = () => true method._post('[]', {}, (error) => { - t.error(error) - - t.ok(sendDeflatedMetrics.isDone()) - - t.end() + assert.equal(error, undefined) + assert.equal(collector.isDone('metric_data'), true) + assert.equal(t.nr.headers['content-encoding'].includes('gzip'), true) + end() }) }) - t.test('should respect the compressed_content_encoding config', (t) => { - const sendGzippedMetrics = nock(URL) - .post(generate('metric_data', RUN_ID)) - .matchHeader('Content-Encoding', 'gzip') - .reply(200, { return_value: [] }) - - config.compressed_content_encoding = 'gzip' + await t.test('should use deflate compression when requested', (t, end) => { + const { collector, method } = t.nr + t.nr.config.put_for_data_send = true method._shouldCompress = () => true + method._agent.config.compressed_content_encoding = 'deflate' method._post('[]', {}, (error) => { - t.error(error) - - t.ok(sendGzippedMetrics.isDone()) - t.end() + assert.equal(error, undefined) + assert.equal(collector.isDone('metric_data'), true) + assert.equal(t.nr.headers['content-encoding'].includes('deflate'), true) + end() }) }) - }) - - t.test('unsuccessfully', (t) => { - t.autoend() - - function nockMetric500() { - return nock(URL).post(generate('metric_data', RUN_ID)).reply(500, { return_value: [] }) - } - t.test('should invoke the callback without error', (t) => { - nockMetric500() + await t.test('should respect the compressed_content_encoding config', (t, end) => { + const { collector, method } = t.nr + t.nr.config.put_for_data_send = true + // gzip is the default, so use deflate to give a value to verify. + t.nr.config.compressed_content_encoding = 'deflate' + method._shouldCompress = () => true method._post('[]', {}, (error) => { - t.error(error) - t.end() - }) - }) - - t.test('should include status code in response', (t) => { - const sendMetrics = nockMetric500() - method._post('[]', {}, (error, response) => { - t.error(error) - t.equal(response.status, 500) - t.ok(sendMetrics.isDone()) - - t.end() - }) - }) - }) - - t.test('with an error', (t) => { - t.autoend() - - let thrown = null - let originalSafeRequest = null - - t.beforeEach(() => { - thrown = new Error('whoops!') - originalSafeRequest = method._safeRequest - method._safeRequest = () => { - throw thrown - } - }) - - t.afterEach(() => { - method._safeRequest = originalSafeRequest - }) - - t.test('should not allow the error to go uncaught', (t) => { - method._post('[]', null, (caught) => { - t.equal(caught, thrown) - t.end() - }) - }) - }) - - t.test('parsing successful response', (t) => { - t.autoend() - - const response = { - return_value: 'collector-42.newrelic.com' - } - - t.beforeEach(() => { - const successConfig = new Config({ - ssl: true, - license_key: 'license key here' - }) - - const endpoint = { - host: 'collector.newrelic.com', - port: 443 - } - - const agent = { config: successConfig, metrics: { measureBytes() {} } } - method = new RemoteMethod('preconnect', agent, endpoint) - - nock(URL).post(generate('preconnect')).reply(200, response) - }) - - t.test('should not error', (t) => { - method.invoke(null, {}, (error) => { - t.error(error) - - t.end() - }) - }) - - t.test('should find the expected value', (t) => { - method.invoke(null, {}, (error, res) => { - t.equal(res.payload, 'collector-42.newrelic.com') - - t.end() - }) - }) - }) - - t.test('parsing error response', (t) => { - t.autoend() - - const response = {} - - t.beforeEach(() => { - nock(URL).post(generate('metric_data', RUN_ID)).reply(409, response) - }) - - t.test('should include status in callback response', (t) => { - method.invoke([], {}, (error, res) => { - t.error(error) - t.equal(res.status, 409) - - t.end() + assert.equal(error, undefined) + assert.equal(collector.isDone('metric_data'), true) + assert.equal(t.nr.headers['content-encoding'].includes('deflate'), true) + end() }) }) }) }) -tap.test('when generating headers for a plain request', (t) => { - t.autoend() +test('when generating headers for a plain request', async (t) => { + t.beforeEach(async (ctx) => { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' + ctx.nr = {} - let headers = null - let options = null - let method = null + const collector = new Collector({ runId: RUN_ID }) + ctx.nr.collector = collector + await collector.listen() - t.beforeEach(() => { - const config = new Config({ - run_id: 12 - }) - - const endpoint = { - host: 'collector.newrelic.com', - port: '80' - } + ctx.nr.config = new Config({ run_id: RUN_ID }) + ctx.nr.endpoint = { host: collector.host, port: collector.port } const body = 'test☃' - method = new RemoteMethod(body, { config }, endpoint) - - options = { + ctx.nr.method = new RemoteMethod( body, - compressed: false - } + { ...BARE_AGENT, config: ctx.nr.config }, + ctx.nr.endpoint + ) + + ctx.nr.options = { body, compressed: false } + ctx.nr.headers = ctx.nr.method._headers(ctx.nr.options) + }) - headers = method._headers(options) + t.afterEach((ctx) => { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '1' + ctx.nr.collector.close() }) - t.test('should use the content type from the parameter', (t) => { - t.equal(headers['CONTENT-ENCODING'], 'identity') - t.end() + await t.test('should use the content type from the parameter', (t) => { + assert.equal(t.nr.headers['CONTENT-ENCODING'], 'identity') }) - t.test('should generate the content length from the body parameter', (t) => { - t.equal(headers['Content-Length'], 7) - t.end() + await t.test('should generate the content length from the body parameter', (t) => { + assert.equal(t.nr.headers['Content-Length'], 7) }) - t.test('should use a keepalive connection', (t) => { - t.equal(headers.Connection, 'Keep-Alive') - t.end() + await t.test('should use keepalive connection', (t) => { + assert.equal(t.nr.headers.Connection, 'Keep-Alive') }) - t.test('should have the host from the configuration', (t) => { - t.equal(headers.Host, 'collector.newrelic.com') - t.end() + await t.test('should have the host from the configuration', (t) => { + assert.equal(t.nr.headers.Host, t.nr.collector.host) }) - t.test('should tell the server we are sending JSON', (t) => { - t.equal(headers['Content-Type'], 'application/json') - t.end() + await t.test('should tell the server we are sending JSON', (t) => { + assert.equal(t.nr.headers['Content-Type'], 'application/json') }) - t.test('should have a user-agent string', (t) => { - t.ok(headers['User-Agent']) - t.end() + await t.test('should have a user-agent string', (t) => { + assert.equal(t.nr.headers['User-Agent'].startsWith('NewRelic-NodeAgent'), true) }) - t.test('should include stored NR headers in outgoing request headers', (t) => { + await t.test('should include stored NR headers in outgoing request headers', (t) => { + const { method, options } = t.nr options.nrHeaders = { 'X-NR-Run-Token': 'AFBE4546FEADDEAD1243', 'X-NR-Metadata': '12BAED78FC89BAFE1243' } - headers = method._headers(options) - - t.equal(headers['X-NR-Run-Token'], 'AFBE4546FEADDEAD1243') - t.equal(headers['X-NR-Metadata'], '12BAED78FC89BAFE1243') - - t.end() + const headers = method._headers(options) + assert.equal(headers['X-NR-Run-Token'], 'AFBE4546FEADDEAD1243') + assert.equal(headers['X-NR-Metadata'], '12BAED78FC89BAFE1243') }) }) -tap.test('when generating headers for a compressed request', (t) => { - t.autoend() - - let headers = null +test('when generating headers for a compressed request', async (t) => { + t.beforeEach(async (ctx) => { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' + ctx.nr = {} - t.beforeEach(() => { - const config = new Config({ - run_id: 12 - }) + const collector = new Collector({ runId: RUN_ID }) + ctx.nr.collector = collector + await collector.listen() - const endpoint = { - host: 'collector.newrelic.com', - port: '80' - } + ctx.nr.config = new Config({ run_id: RUN_ID }) + ctx.nr.endpoint = { host: collector.host, port: collector.port } const body = 'test☃' - const method = new RemoteMethod(body, { config }, endpoint) - - const options = { + ctx.nr.method = new RemoteMethod( body, - compressed: true - } + { ...BARE_AGENT, config: ctx.nr.config }, + ctx.nr.endpoint + ) - headers = method._headers(options) + ctx.nr.options = { body, compressed: true } + ctx.nr.headers = ctx.nr.method._headers(ctx.nr.options) }) - t.test('should use the content type from the parameter', (t) => { - t.equal(headers['CONTENT-ENCODING'], 'gzip') - t.end() + t.afterEach((ctx) => { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '1' + ctx.nr.collector.close() }) - t.test('should generate the content length from the body parameter', (t) => { - t.equal(headers['Content-Length'], 7) - t.end() + await t.test('should use the content type from the parameter', (t) => { + assert.equal(t.nr.headers['CONTENT-ENCODING'], 'gzip') }) - t.test('should use a keepalive connection', (t) => { - t.equal(headers.Connection, 'Keep-Alive') - t.end() + await t.test('should generate the content length from the body parameter', (t) => { + assert.equal(t.nr.headers['Content-Length'], 7) }) - t.test('should have the host from the configuration', (t) => { - t.equal(headers.Host, 'collector.newrelic.com') - t.end() + await t.test('should use keepalive connection', (t) => { + assert.equal(t.nr.headers.Connection, 'Keep-Alive') }) - t.test('should tell the server we are sending JSON', (t) => { - t.equal(headers['Content-Type'], 'application/json') - t.end() + await t.test('should have the host from the configuration', (t) => { + assert.equal(t.nr.headers.Host, t.nr.collector.host) }) - t.test('should have a user-agent string', (t) => { - t.ok(headers['User-Agent']) - t.end() + await t.test('should tell the server we are sending JSON', (t) => { + assert.equal(t.nr.headers['Content-Type'], 'application/json') }) -}) -tap.test('when generating a request URL', (t) => { - t.autoend() + await t.test('should have a user-agent string', (t) => { + assert.equal(t.nr.headers['User-Agent'].startsWith('NewRelic-NodeAgent'), true) + }) +}) +test('when generating headers request URL', async (t) => { const TEST_RUN_ID = Math.floor(Math.random() * 3000) + 1 const TEST_METHOD = 'TEST_METHOD' const TEST_LICENSE = 'hamburtson' - let config = null - let endpoint = null - let parsed = null - function reconstitute(generated) { - return url.parse(generated, true, false) - } + t.beforeEach(async (ctx) => { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' + ctx.nr = {} - t.beforeEach(() => { - config = new Config({ - license_key: TEST_LICENSE - }) + const collector = new Collector({ runId: RUN_ID }) + ctx.nr.collector = collector + await collector.listen() - endpoint = { - host: 'collector.newrelic.com', - port: 80 - } + ctx.nr.config = new Config({ license_key: TEST_LICENSE }) + ctx.nr.endpoint = { host: collector.host, port: collector.port } - const method = new RemoteMethod(TEST_METHOD, { config }, endpoint) - parsed = reconstitute(method._path()) - }) + ctx.nr.method = new RemoteMethod( + TEST_METHOD, + { ...BARE_AGENT, config: ctx.nr.config }, + ctx.nr.endpoint + ) - t.test('should say that it supports protocol 17', (t) => { - t.equal(parsed.query.protocol_version, '17') - t.end() + ctx.nr.parsed = url.parse(ctx.nr.method._path(), true, false) }) - t.test('should tell the collector it is sending JSON', (t) => { - t.equal(parsed.query.marshal_format, 'json') - t.end() + t.afterEach((ctx) => { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '1' + ctx.nr.collector.close() }) - t.test('should pass through the license key', (t) => { - t.equal(parsed.query.license_key, TEST_LICENSE) - t.end() + await t.test('should say that it supports protocol 17', (t) => { + assert.equal(t.nr.parsed.query.protocol_version, 17) }) - t.test('should include the method', (t) => { - t.equal(parsed.query.method, TEST_METHOD) - t.end() + await t.test('should tell the collector it is sending JSON', (t) => { + assert.equal(t.nr.parsed.query.marshal_format, 'json') }) - t.test('should not include the agent run ID when not set', (t) => { - const method = new RemoteMethod(TEST_METHOD, { config }, endpoint) - parsed = reconstitute(method._path()) - t.notOk(parsed.query.run_id) + await t.test('should pass through the license key', (t) => { + assert.equal(t.nr.parsed.query.license_key, TEST_LICENSE) + }) - t.end() + await t.test('should include the method', (t) => { + assert.equal(t.nr.parsed.query.method, TEST_METHOD) }) - t.test('should include the agent run ID when set', (t) => { - config.run_id = TEST_RUN_ID - const method = new RemoteMethod(TEST_METHOD, { config }, endpoint) - parsed = reconstitute(method._path()) - t.equal(parsed.query.run_id, '' + TEST_RUN_ID) + await t.test('should not include the agent run ID when not set', (t) => { + const method = new RemoteMethod(TEST_METHOD, { config: t.nr.config }, t.nr.endpoint) + const parsed = url.parse(method._path(), true, false) + assert.equal(parsed.query.run_id, undefined) + }) - t.end() + await t.test('should include the agent run ID when set', (t) => { + t.nr.config.run_id = TEST_RUN_ID + const method = new RemoteMethod(TEST_METHOD, { config: t.nr.config }, t.nr.endpoint) + const parsed = url.parse(method._path(), true, false) + assert.equal(parsed.query.run_id, TEST_RUN_ID) }) - t.test('should start with the (old-style) path', (t) => { - t.equal(parsed.pathname.indexOf('/agent_listener/invoke_raw_method'), 0) - t.end() + await t.test('should start with the (old-style) path', (t) => { + assert.equal(t.nr.parsed.pathname.indexOf('/agent_listener/invoke_raw_method'), 0) }) }) -tap.test('when generating the User-Agent string', (t) => { - t.autoend() - +test('when generating the User-Agent string', async (t) => { const TEST_VERSION = '0-test' - let userAgent = null - let version = null - let pkg = null + const pkg = require('../../../package.json') + + t.beforeEach(async (ctx) => { + ctx.nr = {} - t.beforeEach(() => { - pkg = require('../../../package.json') - version = pkg.version + ctx.nr.version = pkg.version pkg.version = TEST_VERSION - const config = new Config({}) - const method = new RemoteMethod('test', { config }, {}) - userAgent = method._userAgent() + ctx.nr.config = new Config({}) + ctx.nr.method = new RemoteMethod('test', { config: ctx.nr.config }, {}) + ctx.nr.userAgent = ctx.nr.method._userAgent() }) - t.afterEach(() => { - pkg.version = version + t.afterEach((ctx) => { + pkg.version = ctx.nr.version }) - t.test('should clearly indicate it is New Relic for Node', (t) => { - t.match(userAgent, 'NewRelic-NodeAgent') - t.end() + await t.test('should clearly indicate it is New Relic for Node', (t) => { + assert.equal(t.nr.userAgent.startsWith('NewRelic-NodeAgent'), true) }) - t.test('should include the agent version', (t) => { - t.match(userAgent, TEST_VERSION) - t.end() + await t.test('should include the agent version', (t) => { + assert.equal(t.nr.userAgent.includes(TEST_VERSION), true) }) - t.test('should include node version', (t) => { - t.match(userAgent, process.versions.node) - t.end() + await t.test('should include node version', (t) => { + assert.equal(t.nr.userAgent.includes(process.versions.node), true) }) - t.test('should include node platform and architecture', (t) => { - t.match(userAgent, process.platform + '-' + process.arch) - t.end() + await t.test('should include node platform and architecture', (t) => { + assert.equal(t.nr.userAgent.includes(process.platform + '-' + process.arch), true) }) }) -tap.test('record data usage supportability metrics', (t) => { - t.autoend() +test('record data usage supportability metrics', async (t) => { + t.beforeEach(async (ctx) => { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' + ctx.nr = {} - let endpoint + const collector = new Collector({ runId: RUN_ID }) + ctx.nr.collector = collector + await collector.listen() - let agent + ctx.nr.config = new Config({ license_key: 'license key here' }) + ctx.nr.endpoint = { host: collector.host, port: collector.port } - t.beforeEach(() => { - agent = helper.instrumentMockedAgent() - endpoint = { - host: agent.config.host, - port: agent.config.port - } + ctx.nr.agent = helper.instrumentMockedAgent(collector.agentConfig) }) - t.afterEach(() => { - agent && helper.unloadAgent(agent) + t.afterEach((ctx) => { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '1' + helper.unloadAgent(ctx.nr.agent) + ctx.nr.collector.close() }) - t.test('should aggregate bytes of uploaded payloads', async (t) => { + await t.test('should aggregate bytes of uploaded payloads', async (t) => { + const { agent, endpoint } = t.nr + const method1 = new RemoteMethod('preconnect', agent, endpoint) const method2 = new RemoteMethod('connect', agent, endpoint) const payload = [{ hello: 'world' }] const expectedSize = 19 - const totalMetric = [2, expectedSize * 2, 0, expectedSize, expectedSize, 722] - const singleMetric = [1, expectedSize, 0, expectedSize, expectedSize, 361] + const totalMetric = [2, expectedSize * 2, 79, expectedSize, expectedSize, 722] + const preconnectMetric = [1, expectedSize, 58, expectedSize, expectedSize, 361] + const connectMetric = [1, expectedSize, 21, expectedSize, expectedSize, 361] + for (const method of [method1, method2]) { await new Promise((resolve, reject) => { - method.invoke(payload, (err) => { - err ? reject(err) : resolve() + method.invoke(payload, (error) => { + error ? reject(error) : resolve() }) }) } - t.assertMetricValues( - { - metrics: agent.metrics - }, + assertMetricValues({ metrics: agent.metrics }, [ + [{ name: NAMES.DATA_USAGE.COLLECTOR }, totalMetric], [ - [ - { - name: NAMES.DATA_USAGE.COLLECTOR - }, - totalMetric - ], - [ - { - name: `${NAMES.DATA_USAGE.PREFIX}/preconnect/${NAMES.DATA_USAGE.SUFFIX}` - }, - singleMetric - ], - [ - { - name: `${NAMES.DATA_USAGE.PREFIX}/connect/${NAMES.DATA_USAGE.SUFFIX}` - }, - singleMetric - ] - ] - ) - - t.end() + { name: `${NAMES.DATA_USAGE.PREFIX}/preconnect/${NAMES.DATA_USAGE.SUFFIX}` }, + preconnectMetric + ], + [{ name: `${NAMES.DATA_USAGE.PREFIX}/connect/${NAMES.DATA_USAGE.SUFFIX}` }, connectMetric] + ]) }) - t.test('should report response size ok', async (t) => { + await t.test('should report response size ok', async (t) => { + const { agent, endpoint } = t.nr + const byteLength = (data) => Buffer.byteLength(JSON.stringify(data), 'utf8') const payload = [{ hello: 'world' }] const response = { hello: 'galaxy' } @@ -871,111 +653,97 @@ tap.test('record data usage supportability metrics', (t) => { const responseSize = byteLength(response) const metric = [1, payloadSize, responseSize, 19, 19, 361] const method = new RemoteMethod('preconnect', agent, endpoint) - // stub call to NR so we can test response payload metrics + + // Stub call to NR so we can test response payload metrics: method._post = (data, nrHeaders, callback) => { callback(null, { payload: response }) } + await new Promise((resolve, reject) => { - method.invoke(payload, (err) => { - err ? reject(err) : resolve() + method.invoke(payload, (error) => { + error ? reject(error) : resolve() }) }) - t.assertMetricValues( - { - metrics: agent.metrics - }, - [ - [ - { - name: NAMES.DATA_USAGE.COLLECTOR - }, - metric - ], - [ - { - name: `${NAMES.SUPPORTABILITY.NODEJS}/Collector/preconnect/${NAMES.DATA_USAGE.SUFFIX}` - }, - metric - ] - ] - ) + assertMetricValues({ metrics: agent.metrics }, [ + [{ name: NAMES.DATA_USAGE.COLLECTOR }, metric], + [{ name: `${NAMES.DATA_USAGE.PREFIX}/preconnect/${NAMES.DATA_USAGE.SUFFIX}` }, metric] + ]) }) - t.test('should record metrics even if posting a payload fails', async (t) => { + await t.test('should record metrics even if posting a payload fails', async (t) => { + const { agent, endpoint } = t.nr + const byteLength = (data) => Buffer.byteLength(JSON.stringify(data), 'utf8') const payload = [{ hello: 'world' }] const payloadSize = byteLength(payload) const metric = [1, payloadSize, 0, 19, 19, 361] const method = new RemoteMethod('preconnect', agent, endpoint) - // stub call to NR so we can test response payload metrics + + // Stub call to NR so we can test response payload metrics: method._post = (data, nrHeaders, callback) => { - const err = new Error('') - callback(err) + callback(Error('')) } + await new Promise((resolve) => { method.invoke(payload, resolve) }) - t.assertMetricValues( - { - metrics: agent.metrics - }, - [ - [ - { - name: NAMES.DATA_USAGE.COLLECTOR - }, - metric - ], - [ - { - name: `${NAMES.DATA_USAGE.PREFIX}/preconnect/${NAMES.DATA_USAGE.SUFFIX}` - }, - metric - ] - ] - ) + assertMetricValues({ metrics: agent.metrics }, [ + [{ name: NAMES.DATA_USAGE.COLLECTOR }, metric], + [{ name: `${NAMES.DATA_USAGE.PREFIX}/preconnect/${NAMES.DATA_USAGE.SUFFIX}` }, metric] + ]) }) }) -tap.test('_safeRequest logging', (t) => { - t.autoend() - t.beforeEach((t) => { - const sandbox = sinon.createSandbox() - const loggerMock = require('../mocks/logger')(sandbox) - const RemoteMethod = proxyquire('../../../lib/collector/remote-method', { - '../logger': { - child: sandbox.stub().callsFake(() => loggerMock) +test('_safeRequest logging', async (t) => { + t.beforeEach((ctx) => { + ctx.nr = {} + + ctx.nr.logs = { + info: [], + trace: [] + } + ctx.nr.logger = { + child() { + return this + }, + info(...args) { + ctx.nr.logs.info.push(args) + }, + trace(...args) { + ctx.nr.logs.trace.push(args) + }, + traceEnabled() { + return true } + } + const RemoteMethod = proxyquire('../../../lib/collector/remote-method', { + '../logger': ctx.nr.logger }) - sandbox.stub(RemoteMethod.prototype, '_request') - t.context.loggerMock = loggerMock - t.context.RemoteMethod = RemoteMethod - t.context.sandbox = sandbox - t.context.options = { - host: 'collector.newrelic.com', + RemoteMethod.prototype._request = () => {} + ctx.nr.RemoteMethod = RemoteMethod + + ctx.nr.options = { + host: 'something', port: 80, - onError: () => {}, - onResponse: () => {}, + onError() {}, + onResponse() {}, body: 'test-body', path: '/nonexistent' } - t.context.config = { license_key: 'shhh-dont-tell', max_payload_size_in_bytes: 10000 } - }) - - t.afterEach((t) => { - const { sandbox } = t.context - sandbox.restore() + ctx.nr.config = { + license_key: 'shhh-dont-tell', + max_payload_size_in_bytes: 10_000 + } }) - t.test('should redact license key in logs', (t) => { - const { RemoteMethod, loggerMock, options, config } = t.context - loggerMock.traceEnabled.returns(true) + await t.test('should redact license key in logs', (t) => { + const { RemoteMethod, options, config } = t.nr const method = new RemoteMethod('test', { config }) method._safeRequest(options) - t.same( - loggerMock.trace.args, + assert.deepStrictEqual( + t.nr.logs.trace, [ [ { body: options.body }, @@ -988,18 +756,18 @@ tap.test('_safeRequest logging', (t) => { ], 'should redact key in trace level log' ) - t.end() }) - t.test('should call logger if trace is not enabled but audit logging is enabled', (t) => { - const { RemoteMethod, loggerMock, options, config } = t.context - loggerMock.traceEnabled.returns(false) + await t.test('should call logger if trace is not enabled but audit logging is enabled', (t) => { + const { RemoteMethod, options, config, logger } = t.nr + logger.traceEnabled = () => false config.logging = { level: 'info' } config.audit_log = { enabled: true, endpoints: ['test'] } + const method = new RemoteMethod('test', { config }) method._safeRequest(options) - t.same( - loggerMock.info.args, + assert.deepStrictEqual( + t.nr.logs.info, [ [ { body: options.body }, @@ -1012,16 +780,15 @@ tap.test('_safeRequest logging', (t) => { ], 'should redact key in trace level log' ) - t.end() }) - t.test('should not call logger if trace or audit logging is not enabled', (t) => { - const { RemoteMethod, loggerMock, options, config } = t.context - loggerMock.traceEnabled.returns(false) + await t.test('should not call logger if trace or audit logging is not enabled', (t) => { + const { RemoteMethod, options, config, logger } = t.nr + logger.traceEnabled = () => false + const method = new RemoteMethod('test', { config }) method._safeRequest(options) - t.ok(loggerMock.trace.callCount === 0, 'should not log outgoing message to collector') - t.ok(loggerMock.info.callCount === 0, 'should not log outgoing message to collector') - t.end() + assert.equal(t.nr.logs.info.length, 0) + assert.equal(t.nr.logs.trace.length, 0) }) }) diff --git a/test/unit/collector/serverless.test.js b/test/unit/collector/serverless.test.js index 3e1954b5a3..f5f7fc2f9d 100644 --- a/test/unit/collector/serverless.test.js +++ b/test/unit/collector/serverless.test.js @@ -1,87 +1,89 @@ /* - * 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 tap = require('tap') - -const os = require('os') -const util = require('util') -const zlib = require('zlib') -const nock = require('nock') -const sinon = require('sinon') -const fs = require('fs') -const fsOpenAsync = util.promisify(fs.open) -const fsUnlinkAsync = util.promisify(fs.unlink) +const test = require('node:test') +const assert = require('node:assert') +const fs = require('node:fs') +const os = require('node:os') +const path = require('node:path') +const zlib = require('node:zlib') const helper = require('../../lib/agent_helper') + +const Collector = require('../../lib/test-collector') const API = require('../../../lib/collector/serverless') const serverfulAPI = require('../../../lib/collector/api') -const path = require('path') -tap.test('ServerlessCollector API', (t) => { - t.autoend() +const RUN_ID = 1337 - let api = null - let agent = null +test('ServerlessCollector API', async (t) => { + async function beforeEach(ctx) { + ctx.nr = {} - function beforeTest() { - nock.disableNetConnect() - agent = helper.loadMockedAgent({ - serverless_mode: { - enabled: true - }, + const collector = new Collector({ runId: RUN_ID }) + ctx.nr.collector = collector + await collector.listen() + + const baseAgentConfig = { + serverless_mode: { enabled: true }, app_name: ['TEST'], license_key: 'license key here' + } + const config = Object.assign({}, baseAgentConfig, collector.agentConfig, { + config: { run_id: RUN_ID } }) - agent.reconfigure = () => {} - agent.setState = () => {} - api = new API(agent) + + ctx.nr.agent = helper.loadMockedAgent(config) + ctx.nr.agent.reconfigure = () => {} + ctx.nr.agent.setState = () => {} + + ctx.nr.api = new API(ctx.nr.agent) + process.env.NEWRELIC_PIPE_PATH = os.devNull } - function afterTest() { - nock.enableNetConnect() - helper.unloadAgent(agent) + function afterEach(ctx) { + helper.unloadAgent(ctx.nr.agent) + ctx.nr.collector.close() } - t.test('has all expected methods shared with the serverful API', (t) => { + await t.test('has all expected methods shared with the serverful API', () => { const serverfulSpecificPublicMethods = new Set(['connect', 'reportSettings']) - const sharedMethods = Object.keys(serverfulAPI.prototype).filter((key) => { - return !key.startsWith('_') && !serverfulSpecificPublicMethods.has(key) - }) - - sharedMethods.forEach((method) => { - t.type(API.prototype[method], 'function', `${method} should exist on serverless collector`) - }) - - t.end() + const sharedMethods = Object.keys(serverfulAPI.prototype).filter( + (key) => key.startsWith('_') === false && serverfulSpecificPublicMethods.has(key) === false + ) + + for (const method of sharedMethods) { + assert.equal( + typeof API.prototype[method], + 'function', + `${method} should exist on serverless collector` + ) + } }) - t.test('#isConnected', (t) => { - t.autoend() - - t.beforeEach(beforeTest) - t.afterEach(afterTest) + await t.test('#isConnected', async (t) => { + t.beforeEach(beforeEach) + t.afterEach(afterEach) - t.test('returns true', (t) => { - t.equal(api.isConnected(), true) - t.end() + await t.test('returns true', (t) => { + const { api } = t.nr + assert.equal(api.isConnected(), true) }) }) - t.test('#shutdown', (t) => { - t.autoend() + await t.test('#shutdown', async (t) => { + t.beforeEach(beforeEach) + t.afterEach(afterEach) - t.beforeEach(beforeTest) - t.afterEach(afterTest) - - t.test('enabled to false', (t) => { - t.equal(api.enabled, true) + await t.test('enabled to false', (t, end) => { + const { api } = t.nr api.shutdown(() => { - t.equal(api.enabled, false) - t.end() + assert.equal(api.enabled, false) + end() }) }) }) @@ -97,192 +99,185 @@ tap.test('ServerlessCollector API', (t) => { { key: 'span_event_data', name: '#spanEvents' }, { key: 'log_event_data', name: '#logEvents' } ] - - testMethods.forEach(({ key, name }) => { - t.test(name, (t) => { - t.autoend() - - t.beforeEach(beforeTest) - t.afterEach(afterTest) - - t.test(`adds ${key} to the payload object`, (t) => { + for (const testMethod of testMethods) { + const { key, name } = testMethod + await t.test(name, async (t) => { + t.beforeEach(beforeEach) + t.afterEach(afterEach) + + await t.test(`adds ${key} to the payload object`, (t) => { + const { api } = t.nr const eventData = { type: key } api.send(key, eventData, () => { - t.same(api.payload[key], eventData) - t.end() + assert.deepStrictEqual(api.payload[key], eventData) }) }) - t.test(`does not add ${key} to the payload object when disabled`, (t) => { - api.enabled = false + await t.test(`does not add ${key} to the payload object when disabled`, (t) => { + const { api } = t.nr const eventData = { type: key } + api.enabled = false api.send(key, eventData, () => { - t.same(api.payload[key], null) - t.end() + assert.equal(api.payload[key], null) }) }) }) - }) - - t.test('#flushPayloadSync', (t) => { - t.autoend() + } - t.beforeEach(beforeTest) - t.afterEach(afterTest) + await t.test('#flushPayloadSync', async (t) => { + t.beforeEach(beforeEach) + t.afterEach(afterEach) - t.test('should base64 encode the gzipped payload synchronously', (t) => { - const testPayload = { - someKey: 'someValue', - buyOne: 'getOne' - } + await t.test('should base64 encode the gzipped payload synchronously', (t) => { + const { api } = t.nr + const testPayload = { someKey: 'someValue', buyOne: 'getOne' } api.payload = testPayload - const oldDoFlush = api.constructor.prototype._doFlush + + let flushed = false api._doFlush = function testFlush(data) { const decoded = JSON.parse(zlib.gunzipSync(Buffer.from(data, 'base64'))) - t.ok(decoded.metadata) - t.ok(decoded.data) - t.same(decoded.data, testPayload) + assert.notEqual(decoded.metadata, undefined) + assert.notEqual(decoded.data, undefined) + assert.deepStrictEqual(decoded.data, testPayload) + flushed = true } api.flushPayloadSync() - t.equal(Object.keys(api.payload).length, 0) - api.constructor.prototype._doFlush = oldDoFlush - - t.end() + assert.equal(Object.keys(api.payload).length, 0) + assert.equal(flushed, true) }) }) - t.test('#flushPayload', (t) => { - t.autoend() - - let outputSpy = null - - t.beforeEach(() => { - // We're using NEWRELIC_PIPE_PATH to output to /dev/null so - // let's check that we are writing to the device. - outputSpy = sinon.spy(fs, 'writeFileSync') - - beforeTest() + await t.test('#flushPayload', async (t) => { + t.beforeEach(async (ctx) => { + await beforeEach(ctx) + + ctx.nr.writeSync = fs.writeFileSync + ctx.nr.outFile = null + ctx.nr.outData = null + fs.writeFileSync = (dest, payload) => { + ctx.nr.outFile = dest + ctx.nr.outData = JSON.parse(payload) + ctx.nr.writeSync(dest, payload) + } }) - - t.afterEach(() => { - outputSpy.restore() - - afterTest() + t.afterEach((ctx) => { + afterEach(ctx) + fs.writeFileSync = ctx.nr.writeSync }) - t.test('compresses full payload and writes formatted to stdout', (t) => { + await t.test('compresses full payload and writes formatted to stdout', (t, end) => { + const { api } = t.nr api.payload = { type: 'test payload' } - api.flushPayload(() => { - const logPayload = JSON.parse(outputSpy.args[0][1]) - - t.type(logPayload, Array) - t.type(logPayload[0], 'number') - - t.equal(logPayload[1], 'NR_LAMBDA_MONITORING') - t.type(logPayload[2], 'string') - - t.end() + const { outFile, outData } = t.nr + assert.equal(outFile, '/dev/null') + assert.equal(Array.isArray(outData), true) + assert.equal(outData[0], 1) + assert.equal(outData[1], 'NR_LAMBDA_MONITORING') + assert.equal(typeof outData[2], 'string') + end() }) }) - t.test('handles very large payload and writes formatted to stdout', (t) => { + await t.test('handles very large payload and writes formatted to stdout', (t, end) => { + const { api } = t.nr api.payload = { type: 'test payload' } - for (let i = 0; i < 4096; i++) { - api.payload[`customMetric${i}`] = Math.floor(Math.random() * 100000) + for (let i = 0; i < 4096; i += 1) { + api.payload[`customMetric${i}`] = Math.floor(Math.random() * 100_000) } api.flushPayload(() => { - let logPayload = null - - logPayload = JSON.parse(outputSpy.args[0][1]) - - const buf = Buffer.from(logPayload[2], 'base64') - - zlib.gunzip(buf, (err, unpack) => { - t.error(err) - const payload = JSON.parse(unpack) - t.ok(payload.data) - t.ok(Object.keys(payload.data).length > 4000) - t.end() + const { outData } = t.nr + const buf = Buffer.from(outData[2], 'base64') + zlib.gunzip(buf, (error, unpacked) => { + assert.equal(error, undefined) + const payload = JSON.parse(unpacked) + assert.notEqual(payload.data, undefined) + assert.equal(Object.keys(payload.data).length > 4000, true) + end() }) }) }) }) }) -tap.test('ServerlessCollector with output to custom pipe', (t) => { - t.autoend() - - const customPath = path.resolve('/tmp', 'custom-output') - - let api = null - let agent = null - let writeFileSyncStub = null +test('ServerlessCollector with output to custom pipe', async (t) => { + t.beforeEach(async (ctx) => { + ctx.nr = {} - t.beforeEach(async () => { - nock.disableNetConnect() - - process.env.NEWRELIC_PIPE_PATH = customPath - const fd = await fsOpenAsync(customPath, 'w') - if (!fd) { - throw new Error('fd is null') + const uniqueId = Math.floor(Math.random() * 100) + '-' + Date.now() + ctx.nr.destPath = path.join(os.tmpdir(), `custom-output-${uniqueId}`) + ctx.nr.destFD = await fs.promises.open(ctx.nr.destPath, 'w') + if (!ctx.nr.destFD) { + throw Error('fd is null') } + process.env.NEWRELIC_PIPE_PATH = ctx.nr.destPath + + const collector = new Collector({ runId: RUN_ID }) + ctx.nr.collector = collector + await collector.listen() - agent = helper.loadMockedAgent({ - serverless_mode: { - enabled: true - }, + const baseAgentConfig = { + serverless_mode: { enabled: true }, app_name: ['TEST'], license_key: 'license key here', - NEWRELIC_PIPE_PATH: customPath + NEWRELIC_PIPE_PATH: ctx.nr.destPath + } + const config = Object.assign({}, baseAgentConfig, collector.agentConfig, { + config: { run_id: RUN_ID } }) - agent.reconfigure = () => {} - agent.setState = () => {} - api = new API(agent) - writeFileSyncStub = sinon.stub(fs, 'writeFileSync').callsFake(() => {}) - }) + ctx.nr.agent = helper.loadMockedAgent(config) + ctx.nr.agent.reconfigure = () => {} + ctx.nr.agent.setState = () => {} - t.afterEach(async () => { - nock.enableNetConnect() - helper.unloadAgent(agent) + ctx.nr.api = new API(ctx.nr.agent) - writeFileSyncStub.restore() + ctx.nr.writeSync = fs.writeFileSync + ctx.nr.outFile = null + ctx.nr.outData = null + fs.writeFileSync = (dest, payload) => { + ctx.nr.outFile = dest + ctx.nr.outData = JSON.parse(payload) + ctx.nr.writeSync(dest, payload) + } + }) - await fsUnlinkAsync(customPath) + t.afterEach(async (ctx) => { + helper.unloadAgent(ctx.nr.agent) + ctx.nr.collector.close() + fs.writeFileSync = ctx.nr.writeSync + await fs.promises.unlink(ctx.nr.destPath) }) - t.test('compresses full payload and writes formatted to stdout', (t) => { + await t.test('compresses full payload and writes formatted to stdout', (t, end) => { + const { api } = t.nr api.payload = { type: 'test payload' } api.flushPayload(() => { - const writtenPayload = JSON.parse(writeFileSyncStub.args[0][1]) - - t.type(writtenPayload, Array) - t.type(writtenPayload[0], 'number') - t.equal(writtenPayload[1], 'NR_LAMBDA_MONITORING') - t.type(writtenPayload[2], 'string') - - t.end() + const { outData } = t.nr + assert.equal(Array.isArray(outData), true) + assert.equal(outData[0], 1) + assert.equal(outData[1], 'NR_LAMBDA_MONITORING') + assert.equal(typeof outData[2], 'string') + end() }) }) - t.test('handles very large payload and writes formatted to stdout', (t) => { - api.payload = { type: 'test payload' } - for (let i = 0; i < 4096; i++) { - api.payload[`customMetric${i}`] = Math.floor(Math.random() * 100000) + await t.test('handles very large payload and writes formatted to stdout', (t, end) => { + const { api } = t.nr + for (let i = 0; i < 4096; i += 1) { + api.payload[`customMetric${i}`] = Math.floor(Math.random() * 100_000) } api.flushPayload(() => { - const writtenPayload = JSON.parse(writeFileSyncStub.getCall(0).args[1]) - const buf = Buffer.from(writtenPayload[2], 'base64') - - zlib.gunzip(buf, (err, unpack) => { - t.error(err) - const payload = JSON.parse(unpack) - t.ok(payload.data) - t.ok(Object.keys(payload.data).length > 4000, `expected to be > 4000`) - t.end() + const { outData } = t.nr + const buf = Buffer.from(outData[2], 'base64') + zlib.gunzip(buf, (error, unpacked) => { + assert.equal(error, undefined) + const payload = JSON.parse(unpacked) + assert.notEqual(payload.data, undefined) + assert.equal(Object.keys(payload.data).length > 4000, true, 'expected to be > 4000') + end() }) }) })