diff --git a/lib/collector/facts.js b/lib/collector/facts.js index 9e0fb371bd..fa2290cbf0 100644 --- a/lib/collector/facts.js +++ b/lib/collector/facts.js @@ -6,13 +6,13 @@ 'use strict' const fetchSystemInfo = require('../system-info') -const logger = require('../logger').child({ component: 'facts' }) +const defaultLogger = require('../logger').child({ component: 'facts' }) const os = require('os') const parseLabels = require('../util/label-parser') module.exports = facts -async function facts(agent, callback) { +async function facts(agent, callback, { logger = defaultLogger } = {}) { const startTime = Date.now() const systemInfoPromise = new Promise((resolve) => { diff --git a/lib/util/label-parser.js b/lib/util/label-parser.js index fb18a7248c..3fc17e8123 100644 --- a/lib/util/label-parser.js +++ b/lib/util/label-parser.js @@ -119,6 +119,7 @@ function truncate(str, max) { for (i = 0; i < str.length; ++i) { const chr = str.charCodeAt(i) if (chr >= 0xd800 && chr <= 0xdbff && i !== str.length) { + // Handle UTF-16 surrogate pairs. i += 1 } diff --git a/test/unit/collector/facts.test.js b/test/unit/collector/facts.test.js new file mode 100644 index 0000000000..6fff7fb7d3 --- /dev/null +++ b/test/unit/collector/facts.test.js @@ -0,0 +1,794 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const test = require('node:test') +const assert = require('node:assert') +const os = require('node:os') +const fs = require('node:fs') +const net = require('node:net') + +const helper = require('../../lib/agent_helper') +const { match } = require('../../lib/custom-assertions') +const sysInfo = require('../../../lib/system-info') +const utilTests = require('../../lib/cross_agent_tests/utilization/utilization_json') +const bootIdTests = require('../../lib/cross_agent_tests/utilization/boot_id') + +const APP_NAMES = ['a', 'c', 'b'] +const DISABLE_ALL_DETECTIONS = { + utilization: { + detect_aws: false, + detect_azure: false, + detect_gcp: false, + detect_pcf: false, + detect_docker: false + } +} +const EXPECTED_FACTS = [ + 'pid', + 'host', + 'language', + 'app_name', + 'labels', + 'utilization', + 'agent_version', + 'environment', + 'settings', + 'high_security', + 'display_host', + 'identifier', + 'metadata', + 'event_harvest_config' +] + +test('fun facts about apps that New Relic is interested in including', async (t) => { + t.beforeEach((ctx) => { + ctx.nr = {} + + const logs = { + debug: [], + trace: [] + } + const logger = { + debug(...args) { + logs.debug.push(args) + }, + trace(...args) { + logs.trace.push(args) + } + } + ctx.nr.logger = logger + ctx.nr.logs = logs + + const facts = require('../../../lib/collector/facts') + ctx.nr.facts = function (agent, callback) { + return facts(agent, callback, { logger: ctx.nr.logger }) + } + + const config = { app_name: [...APP_NAMES] } + ctx.nr.agent = helper.loadMockedAgent(Object.assign(config, DISABLE_ALL_DETECTIONS)) + + // Undo agent helper override. + ctx.nr.agent.config.applications = () => { + return config.app_name + } + }) + + t.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) + }) + + await t.test('the current process ID as `pid`', (t, end) => { + const { agent, facts } = t.nr + facts(agent, (result) => { + assert.equal(result.pid, process.pid) + end() + }) + }) + + await t.test('the current hostname as `host`', (t, end) => { + const { agent, facts } = t.nr + facts(agent, (result) => { + assert.equal(result.host, os.hostname()) + assert.notEqual(result.host, 'localhost') + assert.notEqual(result.host, 'localhost.local') + assert.notEqual(result.host, 'localhost.localdomain') + end() + }) + }) + + await t.test('the agent`s language (as `language`) to be `nodejs`', (t, end) => { + const { agent, facts } = t.nr + facts(agent, (result) => { + assert.equal(result.language, 'nodejs') + end() + }) + }) + + await t.test('an array of one or more application names as `app_name`', (t, end) => { + const { agent, facts } = t.nr + facts(agent, (result) => { + assert.equal(Array.isArray(result.app_name), true) + assert.deepStrictEqual(result.app_name, APP_NAMES) + end() + }) + }) + + await t.test('the module`s version as `agent_version`', (t, end) => { + const { agent, facts } = t.nr + facts(agent, (result) => { + assert.equal(result.agent_version, agent.version) + end() + }) + }) + + await t.test('the environment as nested arrays', (t, end) => { + const { agent, facts } = t.nr + facts(agent, (result) => { + assert.equal(Array.isArray(result.environment), true) + assert.equal(result.environment.length > 1, true) + end() + }) + }) + + await t.test('an `identifier` for this agent', (t, end) => { + const { agent, facts } = t.nr + facts(agent, (result) => { + const { identifier } = result + assert.ok(identifier) + assert.ok(identifier.includes('nodejs')) + // Including the host has negative consequences on the server. + assert.equal(identifier.includes(result.host), false) + assert.ok(identifier.includes([...APP_NAMES].sort().join(','))) + end() + }) + }) + + await t.test('`metadata` with NEW_RELIC_METADATA_-prefixed env vars', (t, end) => { + process.env.NEW_RELIC_METADATA_STRING = 'hello' + process.env.NEW_RELIC_METADATA_BOOL = true + process.env.NEW_RELIC_METADATA_NUMBER = 42 + t.after(() => { + delete process.env.NEW_RELIC_METADATA_STRING + delete process.env.NEW_RELIC_METADATA_BOOL + delete process.env.NEW_RELIC_METADATA_NUMBER + }) + + const { agent, facts } = t.nr + facts(agent, (result) => { + assert.ok(result.metadata) + assert.equal(result.metadata.NEW_RELIC_METADATA_STRING, 'hello') + assert.equal(result.metadata.NEW_RELIC_METADATA_BOOL, 'true') + assert.equal(result.metadata.NEW_RELIC_METADATA_NUMBER, '42') + + const expectedLogs = [ + [ + 'New Relic metadata %o', + { + NEW_RELIC_METADATA_STRING: 'hello', + NEW_RELIC_METADATA_BOOL: 'true', + NEW_RELIC_METADATA_NUMBER: '42' + } + ] + ] + assert.equal(match(t.nr.logs.debug, expectedLogs), true, 'New Relic metadata logged properly') + end() + }) + }) + + await t.test('empty `metadata` object if no metadata env vars found', (t, end) => { + const { agent, facts } = t.nr + facts(agent, (result) => { + assert.equal(match(result.metadata, {}), true) + end() + }) + }) + + await t.test('only returns expected facts', (t, end) => { + const { agent, facts } = t.nr + facts(agent, (result) => { + assert.equal(match(Object.keys(result).sort(), EXPECTED_FACTS.sort()), true) + end() + }) + }) + + await t.test('should convert label object to expected format', (t, end) => { + const { agent, facts } = t.nr + const longKey = '€'.repeat(257) + const longValue = '𝌆'.repeat(257) + agent.config.labels = { + a: 'b', + [longKey]: longValue + } + facts(agent, (result) => { + const expected = [ + { label_type: 'a', label_value: 'b' }, + { label_type: '€'.repeat(255), label_value: '𝌆'.repeat(255) } + ] + assert.equal(match(result.labels, expected), true) + end() + }) + }) + + await t.test('should convert label string to expected format', (t, end) => { + const { agent, facts } = t.nr + const longKey = '€'.repeat(257) + const longValue = '𝌆'.repeat(257) + agent.config.labels = `a: b; ${longKey}: ${longValue}` + facts(agent, (result) => { + const expected = [ + { label_type: 'a', label_value: 'b' }, + { label_type: '€'.repeat(255), label_value: '𝌆'.repeat(255) } + ] + assert.equal(match(result.labels, expected), true) + end() + }) + }) + + // Every call connect needs to use the original values of max_samples_stored as the server overwrites + // these with derived samples based on harvest cycle frequencies + await t.test( + 'should add harvest_limits from their respective config values on every call to generate facts', + (t, end) => { + const { agent, facts } = t.nr + const expectedValue = 10 + agent.config.transaction_events.max_samples_stored = expectedValue + agent.config.custom_insights_events.max_samples_stored = expectedValue + agent.config.error_collector.max_event_samples_stored = expectedValue + agent.config.span_events.max_samples_stored = expectedValue + agent.config.application_logging.forwarding.max_samples_stored = expectedValue + + const expectedHarvestConfig = { + harvest_limits: { + analytic_event_data: expectedValue, + custom_event_data: expectedValue, + error_event_data: expectedValue, + span_event_data: expectedValue, + log_event_data: expectedValue + } + } + + facts(agent, (result) => { + assert.equal(match(result.event_harvest_config, expectedHarvestConfig), true) + end() + }) + } + ) +}) + +test('utilization facts', async (t) => { + const awsInfo = require('../../../lib/utilization/aws-info') + const azureInfo = require('../../../lib/utilization/azure-info') + const gcpInfo = require('../../../lib/utilization/gcp-info') + const kubernetesInfo = require('../../../lib/utilization/kubernetes-info') + const common = require('../../../lib/utilization/common') + + t.beforeEach((ctx) => { + ctx.nr = {} + + const startingEnv = {} + for (const [key, value] of Object.entries(process.env)) { + startingEnv[key] = value + } + ctx.nr.startingEnv = startingEnv + + ctx.nr.startingGetMemory = sysInfo._getMemoryStats + ctx.nr.startingGetProcessor = sysInfo._getProcessorStats + ctx.nr.startingDockerInfo = sysInfo._getDockerContainerId + ctx.nr.startingCommonRequest = common.request + ctx.nr.startingCommonReadProc = common.readProc + + common.readProc = (file, cb) => { + setImmediate(cb, null, null) + } + + ctx.nr.networkInterfaces = os.networkInterfaces + + const facts = require('../../../lib/collector/facts') + ctx.nr.facts = function (agent, callback) { + return facts(agent, callback, { logger: ctx.nr.logger }) + } + + awsInfo.clearCache() + azureInfo.clearCache() + gcpInfo.clearCache() + kubernetesInfo.clearCache() + }) + + t.afterEach((ctx) => { + os.networkInterfaces = ctx.nr.networkInterfaces + sysInfo._getMemoryStats = ctx.nr.startingGetMemory + sysInfo._getProcessorStats = ctx.nr.startingGetProcessor + sysInfo._getDockerContainerId = ctx.nr.startingDockerInfo + common.request = ctx.nr.startingCommonRequest + common.readProc = ctx.nr.startingCommonReadProc + + process.env = ctx.nr.startingEnv + + awsInfo.clearCache() + azureInfo.clearCache() + gcpInfo.clearCache() + }) + + for (const testCase of utilTests) { + await t.test(testCase.testname, (t, end) => { + let mockHostname + let mockRam + let mockProc + let mockVendorMetadata + const config = structuredClone(DISABLE_ALL_DETECTIONS) + + for (const key of Object.keys(testCase)) { + const testValue = testCase[key] + + switch (key) { + case 'input_environment_variables': { + for (const [k, v] of Object.entries(testValue)) { + process.env[k] = v + } + if (Object.hasOwn(testValue, 'KUBERNETES_SERVICE_HOST') === true) { + config.utilization.detect_kubernetes = true + } + break + } + + case 'input_aws_id': + case 'input_aws_type': + case 'input_aws_zone': { + mockVendorMetadata = 'aws' + config.utilization.detect_aws = true + break + } + + case 'input_azure_location': + case 'input_azure_name': + case 'input_azure_id': + case 'input_azure_size': { + mockVendorMetadata = 'azure' + config.utilization.detect_azure = true + break + } + + case 'input_gcp_id': + case 'input_gcp_type': + case 'input_gcp_name': + case 'input_gcp_zone': { + mockVendorMetadata = 'gcp' + config.utilization.detect_gcp = true + break + } + + case 'input_pcf_guid': { + mockVendorMetadata = 'pcf' + process.env.CF_INSTANCE_GUID = testValue + config.utilization.detect_pcf = true + break + } + case 'input_pcf_ip': { + mockVendorMetadata = 'pcf' + process.env.CF_INSTANCE_IP = testValue + config.utilization.detect_pcf = true + break + } + case 'input_pcf_mem_limit': { + process.env.MEMORY_LIMIT = testValue + config.utilization.detect_pcf = true + break + } + + case 'input_kubernetes_id': { + mockVendorMetadata = 'kubernetes' + config.utilization.detect_kubernetes = true + break + } + + case 'input_hostname': { + mockHostname = () => testValue + break + } + + case 'input_total_ram_mib': { + mockRam = () => Promise.resolve(testValue) + break + } + + case 'input_logical_processors': { + mockProc = () => Promise.resolve({ logical: testValue }) + break + } + + case 'input_ip_address': { + mockIpAddresses(testValue) + break + } + + // Ignore these keys. + case 'testname': + case 'input_full_hostname': // We don't collect full hostnames. + case 'expected_output_json': { + break + } + + default: { + throw Error(`Unknown test key "${key}"`) + } + } + } + + const expected = testCase.expected_output_json + // We don't collect full hostnames. + delete expected.full_hostname + + const agent = helper.loadMockedAgent(config) + t.after(() => { + helper.unloadAgent(agent) + }) + + if (mockHostname) { + agent.config.getHostnameSafe = mockHostname + } + if (mockRam) { + sysInfo._getMemoryStats = mockRam + } + if (mockProc) { + sysInfo._getProcessorStats = mockProc + } + if (mockVendorMetadata) { + common.request = makeMockCommonRequest(testCase, mockVendorMetadata) + } + + t.nr.facts(agent, (result) => { + assert.equal(match(result.utilization, expected), true) + end() + }) + + function makeMockCommonRequest(tCase, type) { + return (opts, _agent, cb) => { + assert.equal(_agent, agent) + let payload + switch (type) { + case 'aws': { + payload = { + instanceId: tCase.input_aws_id, + instanceType: tCase.input_aws_type, + availabilityZone: tCase.input_aws_zone + } + break + } + + case 'azure': { + payload = { + location: tCase.input_azure_location, + name: tCase.input_azure_name, + vmId: tCase.input_azure_id, + vmSize: tCase.input_azure_size + } + break + } + + case 'gcp': { + payload = { + id: tCase.input_gcp_id, + machineType: tCase.input_gcp_type, + name: tCase.input_gcp_name, + zone: tCase.input_gcp_zone + } + break + } + } + + setImmediate(cb, null, JSON.stringify(payload)) + } + } + }) + } +}) + +test('boot id facts', async (t) => { + const common = require('../../../lib/utilization/common') + + t.beforeEach((ctx) => { + ctx.nr = {} + + const facts = require('../../../lib/collector/facts') + ctx.nr.facts = function (agent, callback) { + return facts(agent, callback, { logger: ctx.nr.logger }) + } + + ctx.nr.startingGetMemory = sysInfo._getMemoryStats + ctx.nr.startingGetProcessor = sysInfo._getProcessorStats + ctx.nr.startingDockerInfo = sysInfo._getDockerContainerId + ctx.nr.startingCommonReadProc = common.readProc + ctx.nr.startingOsPlatform = os.platform + ctx.nr.startingFsAccess = fs.access + + os.platform = () => { + return 'linux' + } + fs.access = (file, mode, cb) => { + cb(null) + } + }) + + t.afterEach((ctx) => { + sysInfo._getMemoryStats = ctx.nr.startingGetMemory + sysInfo._getProcessorStats = ctx.nr.startingGetProcessor + sysInfo._getDockerContainerId = ctx.nr.startingDockerInfo + common.readProc = ctx.nr.startingCommonReadProc + os.platform = ctx.nr.startingOsPlatform + fs.access = ctx.nr.startingFsAccess + }) + + for (const testCase of bootIdTests) { + await t.test(testCase.testname, (t, end) => { + let agent = null + let mockHostname + let mockRam + let mockProc + let mockReadProc + + for (const key of Object.keys(testCase)) { + const testValue = testCase[key] + + switch (key) { + case 'input_hostname': { + mockHostname = () => testValue + break + } + + case 'input_total_ram_mib': { + mockRam = () => Promise.resolve(testValue) + break + } + + case 'input_logical_processors': { + mockProc = () => Promise.resolve({ logical: testValue }) + break + } + + case 'input_boot_id': { + mockReadProc = (file, cb) => cb(null, testValue, agent) + break + } + + // Ignore these keys. + case 'testname': + case 'expected_output_json': + case 'expected_metrics': { + break + } + + default: { + throw Error(`Unknown test key "${key}"`) + } + } + } + + const expected = testCase.expected_output_json + agent = helper.loadMockedAgent(structuredClone(DISABLE_ALL_DETECTIONS)) + t.after(() => helper.unloadAgent(agent)) + + if (mockHostname) { + agent.config.getHostnameSafe = mockHostname + } + if (mockRam) { + sysInfo._getMemoryStats = mockRam + } + if (mockProc) { + sysInfo._getProcessorStats = mockProc + } + if (mockReadProc) { + common.readProc = mockReadProc + } + + t.nr.facts(agent, (result) => { + // There are keys in the facts that aren't account for in the + // expected object (namely ip addreses). + for (const [key, value] of Object.entries(expected)) { + assert.equal(result.utilization[key], value) + } + checkMetrics(testCase.expected_metrics, agent) + end() + }) + }) + } + + function checkMetrics(expectedMetrics, agent) { + if (!expectedMetrics) { + return + } + + for (const [key, value] of Object.entries(expectedMetrics)) { + const metric = agent.metrics.getOrCreateMetric(key) + assert.equal(metric.callCount, value.call_count) + } + } +}) + +test('display_host facts', async (t) => { + t.beforeEach((ctx) => { + ctx.nr = {} + + const facts = require('../../../lib/collector/facts') + ctx.nr.facts = function (agent, callback) { + return facts(agent, callback, { logger: ctx.nr.logger }) + } + + ctx.nr.agent = helper.loadMockedAgent(structuredClone(DISABLE_ALL_DETECTIONS)) + ctx.nr.agent.config.utilization = null + + ctx.nr.osNetworkInterfaces = os.networkInterfaces + ctx.nr.osHostname = os.hostname + os.hostname = () => { + throw 'BROKEN' + } + }) + + t.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) + os.hostname = ctx.nr.osHostname + os.networkInterfaces = ctx.nr.osNetworkInterfaces + delete process.env.DYNO + }) + + await t.test('should be set to what the user specifies (happy path)', (t, end) => { + const { agent, facts } = t.nr + agent.config.process_host.display_name = 'test-value' + facts(agent, (result) => { + assert.equal(result.display_host, 'test-value') + end() + }) + }) + + await t.test('should change large hostname of more than 255 bytes to safe value', (t, end) => { + const { agent, facts } = t.nr + agent.config.process_host.display_name = 'lo'.repeat(200) + facts(agent, (result) => { + assert.equal(result.display_host, agent.config.getHostnameSafe()) + end() + }) + }) + + await t.test('should be process.env.DYNO when use_heroku_dyno_names is true', (t, end) => { + const { agent, facts } = t.nr + process.env.DYNO = 'web.1' + agent.config.heroku.use_dyno_names = true + facts(agent, (result) => { + assert.equal(result.display_host, 'web.1') + end() + }) + }) + + await t.test('should ignore process.env.DYNO when use_heroku_dyno_names is false', (t, end) => { + const { agent, facts } = t.nr + process.env.DYNO = 'ignored' + os.hostname = t.nr.osHostname + agent.config.heroku.use_dyno_names = false + facts(agent, (result) => { + assert.equal(result.display_host, os.hostname()) + end() + }) + }) + + await t.test('should be cached along with hostname in config', (t, end) => { + const { agent, facts } = t.nr + agent.config.process_host.display_name = 'test-value' + facts(agent, (result) => { + const displayHost1 = result.display_host + const host1 = result.host + + os.hostname = t.nr.osHostname + agent.config.process_host.display_name = 'test-value2' + + facts(agent, (result2) => { + assert.equal(match(result2.display_host, displayHost1), true) + assert.equal(match(result2.host, host1), true) + + agent.config.clearHostnameCache() + agent.config.clearDisplayHostCache() + + facts(agent, (result3) => { + assert.equal(match(result3.display_host, 'test-value2'), true) + assert.equal(match(result3.host, os.hostname()), true) + + end() + }) + }) + }) + }) + + await t.test('should be set as os.hostname() (if available) when not specified', (t, end) => { + const { agent, facts } = t.nr + os.hostname = t.nr.osHostname + facts(agent, (result) => { + assert.equal(result.display_host, os.hostname()) + end() + }) + }) + + await t.test('should be ipv4 when ipv_preference === 4', (t, end) => { + const { agent, facts } = t.nr + agent.config.process_host.ipv_preference = '4' + facts(agent, (result) => { + assert.equal(net.isIPv4(result.display_host), true) + end() + }) + }) + + await t.test('should be ipv6 when ipv_preference === 6', (t, end) => { + const { agent, facts } = t.nr + if (!agent.config.getIPAddresses().ipv6) { + t.diagnostic('this machine does not have an ipv6 address, skipping') + return end() + } + + agent.config.process_host.ipv_preference = '6' + facts(agent, (result) => { + assert.equal(net.isIPv6(result.display_host), true) + end() + }) + }) + + await t.test('should be ipv4 when invalid ipv_preference', (t, end) => { + const { agent, facts } = t.nr + agent.config.process_host.ipv_preference = '9' + facts(agent, (result) => { + assert.equal(net.isIPv4(result.display_host), true) + end() + }) + }) + + await t.test('returns no ipv4, hostname should be ipv6 if possible', (t, end) => { + const { agent, facts } = t.nr + if (!agent.config.getIPAddresses().ipv6) { + t.diagnostic('this machine does not have an ipv6 address, skipping') + return end() + } + + const mockedNI = { + lo: [], + en0: [ + { + address: 'fe80::a00:27ff:fe4e:66a1', + netmask: 'ffff:ffff:ffff:ffff::', + family: 'IPv6', + mac: '01:02:03:0a:0b:0c', + internal: false + } + ] + } + os.networkInterfaces = () => mockedNI + + facts(agent, (result) => { + assert.equal(net.isIPv6(result.display_host), true) + end() + }) + }) + + await t.test( + 'returns no ip addresses, hostname should be UNKNOWN_BOX (everything broke)', + (t, end) => { + const { agent, facts } = t.nr + const mockedNI = { lo: [], en0: [] } + os.networkInterfaces = () => mockedNI + facts(agent, (result) => { + assert.equal(result.display_host, 'UNKNOWN_BOX') + end() + }) + } + ) +}) + +function mockIpAddresses(values) { + os.networkInterfaces = () => { + return { + en0: values.reduce((interfaces, address) => { + interfaces.push({ address }) + return interfaces + }, []) + } + } +} diff --git a/test/unit/facts.test.js b/test/unit/facts.test.js deleted file mode 100644 index aa79dea051..0000000000 --- a/test/unit/facts.test.js +++ /dev/null @@ -1,809 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const tap = require('tap') -const fs = require('fs') -const fsAccess = fs.access -const os = require('os') -const hostname = os.hostname -const networkInterfaces = os.networkInterfaces -const helper = require('../lib/agent_helper') -const sinon = require('sinon') -const proxyquire = require('proxyquire') -const loggerMock = require('./mocks/logger')() -const facts = proxyquire('../../lib/collector/facts', { - '../logger': { - child: sinon.stub().callsFake(() => loggerMock) - } -}) -const sysInfo = require('../../lib/system-info') -const utilTests = require('../lib/cross_agent_tests/utilization/utilization_json') -const bootIdTests = require('../lib/cross_agent_tests/utilization/boot_id') - -const EXPECTED = [ - 'pid', - 'host', - 'language', - 'app_name', - 'labels', - 'utilization', - 'agent_version', - 'environment', - 'settings', - 'high_security', - 'display_host', - 'identifier', - 'metadata', - 'event_harvest_config' -] - -const ip6Digits = '(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])' -const ip6Nums = '(?:(?:' + ip6Digits + '.){3,3}' + ip6Digits + ')' -const IP_V6_PATTERN = new RegExp( - '(?:(?:[0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|' + - '(?:[0-9a-fA-F]{1,4}:){1,7}:|' + - '(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|' + - '(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|' + - '(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|' + - '(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|' + - '(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|' + - '[0-9a-fA-F]{1,4}:(?:(?::[0-9a-fA-F]{1,4}){1,6})|' + - ':(?:(?::[0-9a-fA-F]{1,4}){1,7}|:)|' + - 'fe80:(?::[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|' + - '::(?:ffff(?::0{1,4}){0,1}:){0,1}(?:' + - ip6Nums + - ')|' + - '(?:[0-9a-fA-F]{1,4}:){1,4}:(?:' + - ip6Nums + - '))' -) - -const IP_V4_PATTERN = new RegExp( - '(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}' + - '(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])' -) - -const DISABLE_ALL_DETECTIONS = { - utilization: { - detect_aws: false, - detect_azure: false, - detect_gcp: false, - detect_pcf: false, - detect_docker: false - } -} - -const APP_NAMES = ['a', 'c', 'b'] - -tap.test('fun facts about apps that New Relic is interested in include', (t) => { - t.autoend() - - let agent = null - - t.beforeEach(() => { - loggerMock.debug.reset() - const config = { - app_name: [...APP_NAMES] - } - agent = helper.loadMockedAgent(Object.assign(config, DISABLE_ALL_DETECTIONS)) - // Undo agent helper override. - agent.config.applications = () => { - return config.app_name - } - }) - - t.afterEach(() => { - helper.unloadAgent(agent) - os.networkInterfaces = networkInterfaces - }) - - t.test("the current process ID as 'pid'", (t) => { - facts(agent, function getFacts(factsed) { - t.equal(factsed.pid, process.pid) - t.end() - }) - }) - - t.test("the current hostname as 'host' (hope it's not 'localhost' lol)", (t) => { - facts(agent, function getFacts(factsed) { - t.equal(factsed.host, hostname()) - t.not(factsed.host, 'localhost') - t.not(factsed.host, 'localhost.local') - t.not(factsed.host, 'localhost.localdomain') - t.end() - }) - }) - - t.test("the agent's language (as 'language') to be 'nodejs'", (t) => { - facts(agent, function getFacts(factsed) { - t.equal(factsed.language, 'nodejs') - t.end() - }) - }) - - t.test("an array of one or more application names as 'app_name' (sic)", (t) => { - facts(agent, function getFacts(factsed) { - t.ok(Array.isArray(factsed.app_name)) - t.equal(factsed.app_name.length, APP_NAMES.length) - t.end() - }) - }) - - t.test("the module's version as 'agent_version'", (t) => { - facts(agent, function getFacts(factsed) { - t.equal(factsed.agent_version, agent.version) - t.end() - }) - }) - - t.test('the environment (see environment.test.js) as crazy nested arrays', (t) => { - facts(agent, function getFacts(factsed) { - t.ok(Array.isArray(factsed.environment)) - t.ok(factsed.environment.length > 1) - t.end() - }) - }) - - t.test("an 'identifier' for this agent", (t) => { - facts(agent, function (factsed) { - t.ok(factsed.identifier) - const { identifier } = factsed - t.ok(identifier.includes('nodejs')) - // Including the host has negative consequences on the server. - t.notOk(identifier.includes(factsed.host)) - t.ok(identifier.includes([...APP_NAMES].sort().join(','))) - t.end() - }) - }) - - t.test("'metadata' with NEW_RELIC_METADATA_-prefixed env vars", (t) => { - process.env.NEW_RELIC_METADATA_STRING = 'hello' - process.env.NEW_RELIC_METADATA_BOOL = true - process.env.NEW_RELIC_METADATA_NUMBER = 42 - - facts(agent, (data) => { - t.ok(data.metadata) - t.equal(data.metadata.NEW_RELIC_METADATA_STRING, 'hello') - t.equal(data.metadata.NEW_RELIC_METADATA_BOOL, 'true') - t.equal(data.metadata.NEW_RELIC_METADATA_NUMBER, '42') - t.same( - loggerMock.debug.args, - [ - [ - 'New Relic metadata %o', - { - NEW_RELIC_METADATA_STRING: 'hello', - NEW_RELIC_METADATA_BOOL: 'true', - NEW_RELIC_METADATA_NUMBER: '42' - } - ] - ], - 'New relic metadata not logged properly' - ) - - delete process.env.NEW_RELIC_METADATA_STRING - delete process.env.NEW_RELIC_METADATA_BOOL - delete process.env.NEW_RELIC_METADATA_NUMBER - t.end() - }) - }) - - t.test("empty 'metadata' object if no metadata env vars found", (t) => { - facts(agent, (data) => { - t.same(data.metadata, {}) - t.end() - }) - }) - - t.test('and nothing else', (t) => { - facts(agent, function getFacts(factsed) { - t.same(Object.keys(factsed).sort(), EXPECTED.sort()) - t.end() - }) - }) - - t.test('should convert label object to expected format', (t) => { - const longKey = Array(257).join('€') - const longValue = Array(257).join('𝌆') - agent.config.labels = {} - agent.config.labels.a = 'b' - agent.config.labels[longKey] = longValue - facts(agent, function getFacts(factsed) { - const expected = [{ label_type: 'a', label_value: 'b' }] - expected.push({ - label_type: Array(256).join('€'), - label_value: Array(256).join('𝌆') - }) - - t.same(factsed.labels, expected) - t.end() - }) - }) - - t.test('should convert label string to expected format', (t) => { - const longKey = Array(257).join('€') - const longValue = Array(257).join('𝌆') - agent.config.labels = 'a: b; ' + longKey + ' : ' + longValue - facts(agent, function getFacts(factsed) { - const expected = [{ label_type: 'a', label_value: 'b' }] - expected.push({ - label_type: Array(256).join('€'), - label_value: Array(256).join('𝌆') - }) - - t.same(factsed.labels, expected) - t.end() - }) - }) - - // Every call connect needs to use the original values of max_samples_stored as the server overwrites - // these with derived samples based on harvest cycle frequencies - t.test( - 'should add harvest_limits from their respective config values on every call to generate facts', - (t) => { - const expectedValue = 10 - agent.config.transaction_events.max_samples_stored = expectedValue - agent.config.custom_insights_events.max_samples_stored = expectedValue - agent.config.error_collector.max_event_samples_stored = expectedValue - agent.config.span_events.max_samples_stored = expectedValue - agent.config.application_logging.forwarding.max_samples_stored = expectedValue - - const expectedHarvestConfig = { - harvest_limits: { - analytic_event_data: expectedValue, - custom_event_data: expectedValue, - error_event_data: expectedValue, - span_event_data: expectedValue, - log_event_data: expectedValue - } - } - - facts(agent, (factsResult) => { - t.same(factsResult.event_harvest_config, expectedHarvestConfig) - t.end() - }) - } - ) -}) - -tap.test('utilization', (t) => { - t.autoend() - - let agent = null - const awsInfo = require('../../lib/utilization/aws-info') - const azureInfo = require('../../lib/utilization/azure-info') - const gcpInfo = require('../../lib/utilization/gcp-info') - const kubernetesInfo = require('../../lib/utilization/kubernetes-info') - const common = require('../../lib/utilization/common') - - let startingEnv = null - let startingGetMemory = null - let startingGetProcessor = null - let startingDockerInfo = null - let startingCommonRequest = null - let startingCommonReadProc = null - - t.beforeEach(() => { - startingEnv = {} - Object.keys(process.env).forEach((key) => { - startingEnv[key] = process.env[key] - }) - - startingGetMemory = sysInfo._getMemoryStats - startingGetProcessor = sysInfo._getProcessorStats - startingDockerInfo = sysInfo._getDockerContainerId - startingCommonRequest = common.request - startingCommonReadProc = common.readProc - - common.readProc = (file, cb) => { - setImmediate(cb, null, null) - } - - awsInfo.clearCache() - azureInfo.clearCache() - gcpInfo.clearCache() - kubernetesInfo.clearCache() - }) - - t.afterEach(() => { - if (agent) { - helper.unloadAgent(agent) - } - - os.networkInterfaces = networkInterfaces - process.env = startingEnv - sysInfo._getMemoryStats = startingGetMemory - sysInfo._getProcessorStats = startingGetProcessor - sysInfo._getDockerContainerId = startingDockerInfo - common.request = startingCommonRequest - common.readProc = startingCommonReadProc - - startingEnv = null - startingGetMemory = null - startingGetProcessor = null - startingDockerInfo = null - startingCommonRequest = null - startingCommonReadProc = null - - awsInfo.clearCache() - azureInfo.clearCache() - gcpInfo.clearCache() - }) - - utilTests.forEach((test) => { - t.test(test.testname, (t) => { - let mockHostname = false - let mockRam = false - let mockProc = false - let mockVendorMetadata = false - const config = { - utilization: { - detect_aws: false, - detect_azure: false, - detect_gcp: false, - detect_pcf: false, - detect_docker: false, - detect_kubernetes: false - } - } - - Object.keys(test).forEach(function setVal(key) { - const testValue = test[key] - - switch (key) { - case 'input_environment_variables': - Object.keys(testValue).forEach((name) => { - process.env[name] = testValue[name] - }) - - if (testValue.hasOwnProperty('KUBERNETES_SERVICE_HOST')) { - config.utilization.detect_kubernetes = true - } - break - - case 'input_aws_id': - case 'input_aws_type': - case 'input_aws_zone': - mockVendorMetadata = 'aws' - config.utilization.detect_aws = true - break - - case 'input_azure_location': - case 'input_azure_name': - case 'input_azure_id': - case 'input_azure_size': - mockVendorMetadata = 'azure' - config.utilization.detect_azure = true - break - - case 'input_gcp_id': - case 'input_gcp_type': - case 'input_gcp_name': - case 'input_gcp_zone': - mockVendorMetadata = 'gcp' - config.utilization.detect_gcp = true - break - - case 'input_pcf_guid': - mockVendorMetadata = 'pcf' - process.env.CF_INSTANCE_GUID = testValue - config.utilization.detect_pcf = true - break - case 'input_pcf_ip': - mockVendorMetadata = 'pcf' - process.env.CF_INSTANCE_IP = testValue - config.utilization.detect_pcf = true - break - case 'input_pcf_mem_limit': - process.env.MEMORY_LIMIT = testValue - config.utilization.detect_pcf = true - break - - case 'input_kubernetes_id': - mockVendorMetadata = 'kubernetes' - config.utilization.detect_kubernetes = true - break - - case 'input_hostname': - mockHostname = () => testValue - break - - case 'input_total_ram_mib': - mockRam = async () => Promise.resolve(testValue) - break - - case 'input_logical_processors': - mockProc = async () => Promise.resolve({ logical: testValue }) - break - - case 'input_ip_address': - mockIpAddresses(testValue) - break - - // Ignore these keys. - case 'testname': - case 'input_full_hostname': // We don't collect full hostnames - case 'expected_output_json': - break - - default: - throw new Error('Unknown test key "' + key + '"') - } - }) - - const expected = test.expected_output_json - // We don't collect full hostnames - delete expected.full_hostname - - agent = helper.loadMockedAgent(config) - if (mockHostname) { - agent.config.getHostnameSafe = mockHostname - mockHostname = false - } - if (mockRam) { - sysInfo._getMemoryStats = mockRam - mockRam = false - } - if (mockProc) { - sysInfo._getProcessorStats = mockProc - mockProc = false - } - if (mockVendorMetadata) { - common.request = makeMockCommonRequest(t, test, mockVendorMetadata) - } - facts(agent, function getFacts(factsed) { - t.same(factsed.utilization, expected) - t.end() - }) - }) - }) - - function makeMockCommonRequest(t, test, type) { - return (opts, _agent, cb) => { - t.equal(_agent, agent) - let payload = null - switch (type) { - case 'aws': { - payload = { - instanceId: test.input_aws_id, - instanceType: test.input_aws_type, - availabilityZone: test.input_aws_zone - } - break - } - case 'azure': { - payload = { - location: test.input_azure_location, - name: test.input_azure_name, - vmId: test.input_azure_id, - vmSize: test.input_azure_size - } - break - } - case 'gcp': { - payload = { - id: test.input_gcp_id, - machineType: test.input_gcp_type, - name: test.input_gcp_name, - zone: test.input_gcp_zone - } - break - } - } - setImmediate(cb, null, JSON.stringify(payload)) - } - } -}) - -tap.test('boot_id', (t) => { - t.autoend() - let agent = null - const common = require('../../lib/utilization/common') - - let startingGetMemory = null - let startingGetProcessor = null - let startingDockerInfo = null - let startingCommonReadProc = null - let startingOsPlatform = null - - t.beforeEach(() => { - startingGetMemory = sysInfo._getMemoryStats - startingGetProcessor = sysInfo._getProcessorStats - startingDockerInfo = sysInfo._getDockerContainerId - startingCommonReadProc = common.readProc - startingOsPlatform = os.platform - - os.platform = () => 'linux' - fs.access = (file, mode, cb) => cb(null) - }) - - t.afterEach(() => { - if (agent) { - helper.unloadAgent(agent) - } - - sysInfo._getMemoryStats = startingGetMemory - sysInfo._getProcessorStats = startingGetProcessor - sysInfo._getDockerContainerId = startingDockerInfo - common.readProc = startingCommonReadProc - os.platform = startingOsPlatform - fs.access = fsAccess - - startingGetMemory = null - startingGetProcessor = null - startingDockerInfo = null - startingCommonReadProc = null - startingOsPlatform = null - }) - - bootIdTests.forEach((test) => { - t.test(test.testname, (t) => { - let mockHostname = false - let mockRam = false - let mockProc = false - let mockReadProc = false - - Object.keys(test).forEach(function setVal(key) { - const testValue = test[key] - - switch (key) { - case 'input_hostname': - mockHostname = () => testValue - break - - case 'input_total_ram_mib': - mockRam = async () => Promise.resolve(testValue) - break - - case 'input_logical_processors': - mockProc = async () => Promise.resolve({ logical: testValue }) - break - - case 'input_boot_id': - mockReadProc = (file, cb) => { - cb(null, testValue, agent) - } - break - - // Ignore these keys. - case 'testname': - case 'expected_output_json': - case 'expected_metrics': - break - - default: - throw new Error('Unknown test key "' + key + '"') - } - }) - - const expected = test.expected_output_json - - agent = helper.loadMockedAgent(DISABLE_ALL_DETECTIONS) - if (mockHostname) { - agent.config.getHostnameSafe = mockHostname - mockHostname = false - } - if (mockRam) { - sysInfo._getMemoryStats = mockRam - mockRam = false - } - if (mockProc) { - sysInfo._getProcessorStats = mockProc - mockProc = false - } - if (mockReadProc) { - common.readProc = mockReadProc - } - facts(agent, function getFacts(factsed) { - // There are keys in the facts that aren't accounted for in the - // expected object (namely ip addresses). - Object.keys(expected).forEach((key) => { - t.equal(factsed.utilization[key], expected[key]) - }) - checkMetrics(test.expected_metrics) - t.end() - }) - }) - }) - - function checkMetrics(expectedMetrics) { - if (!expectedMetrics) { - return - } - - Object.keys(expectedMetrics).forEach((expectedMetric) => { - const metric = agent.metrics.getOrCreateMetric(expectedMetric) - t.equal(metric.callCount, expectedMetrics[expectedMetric].call_count) - }) - } -}) - -tap.test('display_host', { timeout: 20000 }, (t) => { - t.autoend() - - const originalHostname = os.hostname - - let agent = null - - t.beforeEach(() => { - agent = helper.loadMockedAgent(DISABLE_ALL_DETECTIONS) - agent.config.utilization = null - os.hostname = () => { - throw 'BROKEN' - } - }) - - t.afterEach(() => { - os.hostname = originalHostname - helper.unloadAgent(agent) - delete process.env.DYNO - - agent = null - }) - - t.test('should be set to what the user specifies (happy path)', (t) => { - agent.config.process_host.display_name = 'test-value' - facts(agent, function getFacts(factsed) { - t.equal(factsed.display_host, 'test-value') - t.end() - }) - }) - - t.test('should change large hostname of more than 255 bytes to safe value', (t) => { - agent.config.process_host.display_name = 'lo'.repeat(200) - facts(agent, function getFacts(factsed) { - t.equal(factsed.display_host, agent.config.getHostnameSafe()) - t.end() - }) - }) - - t.test('should be process.env.DYNO when use_heroku_dyno_names is true', (t) => { - process.env.DYNO = 'web.1' - agent.config.heroku.use_dyno_names = true - facts(agent, function getFacts(factsed) { - t.equal(factsed.display_host, 'web.1') - t.end() - }) - }) - - t.test('should ignore process.env.DYNO when use_heroku_dyno_names is false', (t) => { - process.env.DYNO = 'web.1' - os.hostname = originalHostname - agent.config.heroku.use_dyno_names = false - facts(agent, function getFacts(factsed) { - t.equal(factsed.display_host, os.hostname()) - t.end() - }) - }) - - t.test('should be cached along with hostname in config', (t) => { - agent.config.process_host.display_name = 'test-value' - facts(agent, function getFacts(factsed) { - const displayHost1 = factsed.display_host - const host1 = factsed.host - - os.hostname = originalHostname - agent.config.process_host.display_name = 'test-value2' - - facts(agent, function getFacts2(factsed2) { - t.same(factsed2.display_host, displayHost1) - t.same(factsed2.host, host1) - - agent.config.clearHostnameCache() - agent.config.clearDisplayHostCache() - - facts(agent, function getFacts3(factsed3) { - t.same(factsed3.display_host, 'test-value2') - t.same(factsed3.host, os.hostname()) - - t.end() - }) - }) - }) - }) - - t.test('should be set as os.hostname() (if available) when not specified', (t) => { - os.hostname = originalHostname - facts(agent, function getFacts(factsed) { - t.equal(factsed.display_host, os.hostname()) - t.end() - }) - }) - - t.test('should be ipv4 when ipv_preference === 4', (t) => { - agent.config.process_host.ipv_preference = '4' - - facts(agent, function getFacts(factsed) { - t.match(factsed.display_host, IP_V4_PATTERN) - t.end() - }) - }) - - t.test('should be ipv6 when ipv_preference === 6', (t) => { - if (!agent.config.getIPAddresses().ipv6) { - /* eslint-disable no-console */ - console.log('this machine does not have an ipv6 address, skipping') - /* eslint-enable no-console */ - return t.end() - } - agent.config.process_host.ipv_preference = '6' - - facts(agent, function getFacts(factsed) { - t.match(factsed.display_host, IP_V6_PATTERN) - t.end() - }) - }) - - t.test('should be ipv4 when invalid ipv_preference', (t) => { - agent.config.process_host.ipv_preference = '9' - - facts(agent, function getFacts(factsed) { - t.match(factsed.display_host, IP_V4_PATTERN) - - t.end() - }) - }) - - t.test('returns no ipv4, hostname should be ipv6 if possible', (t) => { - if (!agent.config.getIPAddresses().ipv6) { - /* eslint-disable no-console */ - console.log('this machine does not have an ipv6 address, skipping') - /* eslint-enable no-console */ - return t.end() - } - const mockedNI = { - lo: [], - en0: [ - { - address: 'fe80::a00:27ff:fe4e:66a1', - netmask: 'ffff:ffff:ffff:ffff::', - family: 'IPv6', - mac: '01:02:03:0a:0b:0c', - internal: false - } - ] - } - const originalNI = os.networkInterfaces - os.networkInterfaces = createMock(mockedNI) - - facts(agent, function getFacts(factsed) { - t.match(factsed.display_host, IP_V6_PATTERN) - os.networkInterfaces = originalNI - - t.end() - }) - }) - - t.test('returns no ip addresses, hostname should be UNKNOWN_BOX (everything broke)', (t) => { - const mockedNI = { lo: [], en0: [] } - const originalNI = os.networkInterfaces - os.networkInterfaces = createMock(mockedNI) - - facts(agent, function getFacts(factsed) { - os.networkInterfaces = originalNI - t.equal(factsed.display_host, 'UNKNOWN_BOX') - t.end() - }) - }) -}) - -function createMock(output) { - return function mock() { - return output - } -} - -function mockIpAddresses(values) { - os.networkInterfaces = () => { - return { - en0: values.reduce((interfaces, address) => { - interfaces.push({ address }) - return interfaces - }, []) - } - } -} diff --git a/test/unit/feature_flag.test.js b/test/unit/feature_flag.test.js index 2abb5f0368..12dcff62ae 100644 --- a/test/unit/feature_flag.test.js +++ b/test/unit/feature_flag.test.js @@ -1,15 +1,17 @@ /* - * 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 flags = require('../../lib/feature_flags') const Config = require('../../lib/config') -// please do not delete flags from here +// Please do not delete flags from here. const used = [ 'internal_test_only', @@ -41,87 +43,89 @@ const used = [ 'kafkajs_instrumentation' ] -tap.test('feature flags', (t) => { - t.beforeEach(async (t) => { - t.context.prerelease = Object.keys(flags.prerelease) - t.context.unreleased = [...flags.unreleased] - t.context.released = [...flags.released] - }) +test.beforeEach((ctx) => { + ctx.nr = {} + ctx.nr.prerelease = Object.keys(flags.prerelease) + ctx.nr.unreleased = [...flags.unreleased] + ctx.nr.released = [...flags.released] - t.test('should declare every prerelease feature in the *used* variable', async (t) => { - t.context.prerelease.forEach((key) => { - t.equal(used.includes(key), true) - }) - }) + ctx.nr.setLogger = Config.prototype.setLogger +}) - t.test('should declare every release feature in the *used* variable', async (t) => { - t.context.released.forEach((key) => { - t.equal(used.includes(key), true) - }) - }) +test.afterEach((ctx) => { + Config.prototype.setLogger = ctx.nr.setLogger +}) - t.test('should declare every unrelease feature in the *used* variable', async (t) => { - t.context.unreleased.forEach((key) => { - t.equal(used.includes(key), true) - }) - }) +test('should declare every prerelease feature in the *used* variable', (t) => { + for (const key of t.nr.prerelease) { + assert.equal(used.includes(key), true) + } +}) - t.test('should not re-declare a flag in prerelease from released', async (t) => { - const { prerelease, released } = t.context - const filtered = prerelease.filter((n) => released.includes(n)) - t.equal(filtered.length, 0) - }) +test('should declare every release feature in the *used* variable', (t) => { + for (const key of t.nr.released) { + assert.equal(used.includes(key), true) + } +}) - t.test('should not re-declare a flag in prerelease from unreleased', async (t) => { - const { prerelease, unreleased } = t.context - const filtered = prerelease.filter((n) => unreleased.includes(n)) - t.equal(filtered.length, 0) - }) +test('should declare every unreleased feature in the *used* variable', (t) => { + for (const key of t.nr.unreleased) { + assert.equal(used.includes(key), true) + } +}) - t.test('should account for all *used* keys', async (t) => { - const { released, unreleased, prerelease } = t.context - used.forEach((key) => { - if (released.includes(key) === true) { - return - } - if (unreleased.includes(key) === true) { - return - } - if (prerelease.includes(key) === true) { - return - } - - throw Error('Flag not accounted for') - }) - }) +test('should not re-declare a flag in prerelease from released', (t) => { + const { prerelease, released } = t.nr + const filtered = prerelease.filter((n) => released.includes(n)) + assert.equal(filtered.length, 0) +}) - t.test('should warn if released flags are still in config', async (t) => { - let called = false - Config.prototype.setLogger({ - warn: () => { - called = true - }, - warnOnce: () => {} - }) - const config = new Config() - config.feature_flag.released = true - config.validateFlags() - t.equal(called, true) - }) +test('should not re-declare a flag in prerelease from unreleased', (t) => { + const { prerelease, unreleased } = t.nr + const filtered = prerelease.filter((n) => unreleased.includes(n)) + assert.equal(filtered.length, 0) +}) + +test('should account for all *used* keys', (t) => { + const { released, unreleased, prerelease } = t.nr + for (const key of used) { + if (released.includes(key) === true) { + continue + } + if (unreleased.includes(key) === true) { + continue + } + if (prerelease.includes(key) === true) { + continue + } + throw Error(`Flag "${key}" not accounted for.`) + } +}) - t.test('should warn if unreleased flags are still in config', async (t) => { - let called = false - Config.prototype.setLogger({ - warn: () => { - called = true - }, - warnOnce: () => {} - }) - const config = new Config() - config.feature_flag.unreleased = true - config.validateFlags() - t.equal(called, true) +test('should warn if released flags are still in config', () => { + let called = false + Config.prototype.setLogger({ + warn() { + called = true + }, + warnOnce() {} }) + const config = new Config() + config.feature_flag.released = true + config.validateFlags() + assert.equal(called, true) +}) - t.end() +test('should warn if unreleased flags are still in config', () => { + let called = false + Config.prototype.setLogger({ + warn() { + called = true + }, + warnOnce() {} + }) + const config = new Config() + config.feature_flag.unreleased = true + config.validateFlags() + assert.equal(called, true) }) diff --git a/test/unit/harvester.test.js b/test/unit/harvester.test.js index 347531219f..abcea8d261 100644 --- a/test/unit/harvester.test.js +++ b/test/unit/harvester.test.js @@ -5,11 +5,15 @@ 'use strict' -const tap = require('tap') -const Harvester = require('../../lib/harvester') -const { EventEmitter } = require('events') +const test = require('node:test') +const assert = require('node:assert') +const { EventEmitter } = require('node:events') const sinon = require('sinon') +const { match } = require('../lib/custom-assertions') +const promiseResolvers = require('../lib/promise-resolvers') +const Harvester = require('../../lib/harvester') + class FakeAggregator extends EventEmitter { constructor(opts) { super() @@ -36,69 +40,67 @@ function createAggregator(sandbox, opts) { return aggregator } -tap.beforeEach((t) => { +test.beforeEach((ctx) => { + ctx.nr = {} + const sandbox = sinon.createSandbox() const aggregators = [ createAggregator(sandbox, { enabled: true, method: 'agg1' }), createAggregator(sandbox, { enabled: false, method: 'agg2' }) ] const harvester = new Harvester() - aggregators.forEach((aggregator) => { - harvester.add(aggregator) - }) - t.context.sandbox = sandbox - t.context.aggregators = aggregators - t.context.harvester = harvester + aggregators.forEach((a) => harvester.add(a)) + + ctx.nr.sandbox = sandbox + ctx.nr.aggregators = aggregators + ctx.nr.harvester = harvester }) -tap.afterEach((t) => { - t.context.sandbox.restore() +test.afterEach((ctx) => { + ctx.nr.sandbox.restore() }) -tap.test('Harvester should have aggregators property', (t) => { +test('should have aggregators property', () => { const harvester = new Harvester() - t.same(harvester.aggregators, []) - t.end() + assert.deepStrictEqual(harvester.aggregators, []) }) -tap.test('Harvester should add aggregator to this.aggregators', (t) => { - const { harvester, aggregators } = t.context - t.ok(harvester.aggregators.length, 2, 'should add 2 aggregators') - t.same(harvester.aggregators, aggregators) - t.end() +test('should add aggregator to this.aggregators', (t) => { + const { harvester, aggregators } = t.nr + assert.equal(harvester.aggregators.length, 2, 'should add 2 aggregators') + assert.deepStrictEqual(harvester.aggregators, aggregators) }) -tap.test('Harvester should start all aggregators that are enabled', (t) => { - const { aggregators, harvester } = t.context +test('should start all aggregators that are enabled', (t) => { + const { harvester, aggregators } = t.nr harvester.start() - t.equal(aggregators[0].start.callCount, 1, 'should start enabled aggregator') - t.equal(aggregators[1].start.callCount, 0, 'should not start disabled aggregator') - t.end() + assert.equal(aggregators[0].start.callCount, 1, 'should start enabled aggregator') + assert.equal(aggregators[1].start.callCount, 0, 'should not start disabled aggregator') }) -tap.test('Harvester should stop all aggregators', (t) => { - const { aggregators, harvester } = t.context +test('should stop all aggregators', (t) => { + const { harvester, aggregators } = t.nr harvester.stop() - t.equal(aggregators[0].stop.callCount, 1, 'should stop enabled aggregator') - t.equal(aggregators[1].stop.callCount, 1, 'should stop disabled aggregator') - t.end() + assert.equal(aggregators[0].stop.callCount, 1, 'should stop enabled aggregator') + assert.equal(aggregators[1].stop.callCount, 1, 'should stop disabled aggregator') }) -tap.test('Harvester should reconfigure all aggregators', (t) => { - const { aggregators, harvester } = t.context +test('should reconfigure all aggregators', (t) => { + const { aggregators, harvester } = t.nr const config = { key: 'value' } harvester.update(config) - t.equal(aggregators[0].reconfigure.callCount, 1, 'should stop enabled aggregator') - t.equal(aggregators[1].reconfigure.callCount, 1, 'should stop disabled aggregator') - t.same(aggregators[0].reconfigure.args[0], [config]) - t.end() + assert.equal(aggregators[0].reconfigure.callCount, 1, 'should stop enabled aggregator') + assert.equal(aggregators[1].reconfigure.callCount, 1, 'should stop disabled aggregator') + assert.equal(match(aggregators[0].reconfigure.args[0], [config]), true) }) -tap.test('should resolve when all data is sent', (t) => { - const { aggregators, harvester } = t.context - harvester.clear(() => { - t.equal(aggregators[0].send.callCount, 1, 'should call send on enabled aggregator') - t.equal(aggregators[1].send.callCount, 0, 'should not call send on disabled aggregator') - t.end() +test('resolve when all data is sent', async (t) => { + const { promise, resolve } = promiseResolvers() + const { aggregators, harvester } = t.nr + await harvester.clear(() => { + assert.equal(aggregators[0].send.callCount, 1, 'should call send on enabled aggregator') + assert.equal(aggregators[1].send.callCount, 0, 'should not call send on disabled aggregator') + resolve() }) + await promise }) diff --git a/test/unit/hashes.test.js b/test/unit/hashes.test.js deleted file mode 100644 index 8267a80465..0000000000 --- a/test/unit/hashes.test.js +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const tap = require('tap') -const testData = require('../lib/obfuscation-data') -const hashes = require('../../lib/util/hashes') - -tap.test('obfuscation', (t) => { - t.test('should objuscate strings correctly', (t) => { - testData.forEach(function (test) { - t.equal(hashes.obfuscateNameUsingKey(test.input, test.key), test.output) - }) - t.end() - }) - t.end() -}) - -tap.test('deobfuscation', (t) => { - t.test('should deobjuscate strings correctly', (t) => { - testData.forEach(function (test) { - t.equal(hashes.deobfuscateNameUsingKey(test.output, test.key), test.input) - }) - t.end() - }) - t.end() -}) - -tap.test('getHash', (t) => { - /** - * TODO: crypto.DEFAULT_ENCODING has been deprecated. - * When fully disabled, this test can likely be removed. - * https://nodejs.org/api/deprecations.html#DEP0091 - */ - /* eslint-disable node/no-deprecated-api */ - t.test('should not crash when changing the DEFAULT_ENCODING key on crypto', (t) => { - const crypto = require('crypto') - const oldEncoding = crypto.DEFAULT_ENCODING - crypto.DEFAULT_ENCODING = 'utf-8' - t.doesNotThrow(hashes.getHash.bind(null, 'TEST_APP', 'TEST_TXN')) - crypto.DEFAULT_ENCODING = oldEncoding - t.end() - }) - /* eslint-enable node/no-deprecated-api */ - t.end() -}) diff --git a/test/unit/header-attributes.test.js b/test/unit/header-attributes.test.js index 89432f0293..470552562c 100644 --- a/test/unit/header-attributes.test.js +++ b/test/unit/header-attributes.test.js @@ -1,15 +1,21 @@ /* - * 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 helper = require('../lib/agent_helper') const headerAttributes = require('../../lib/header-attributes') -const DESTINATIONS = require('../../lib/config/attribute-filter').DESTINATIONS -function beforeEach(t) { +const { DESTINATIONS } = require('../../lib/config/attribute-filter') + +function beforeEach(ctx) { + ctx.nr = {} + const config = { attributes: { exclude: [ @@ -26,41 +32,121 @@ function beforeEach(t) { ] } } - t.context.agent = helper.loadMockedAgent(config) + ctx.nr.agent = helper.loadMockedAgent(config) } -function afterEach(t) { - helper.unloadAgent(t.context.agent) +function afterEach(ctx) { + helper.unloadAgent(ctx.nr.agent) } -tap.test('header-attributes', (t) => { - t.autoend() +test('#collectRequestHeaders', async (t) => { + t.beforeEach(beforeEach) + t.afterEach(afterEach) + + await t.test('should be case insensitive when allow_all_headers is false', (t, end) => { + const { agent } = t.nr + agent.config.allow_all_headers = false + const headers = { + Accept: 'acceptValue' + } + + helper.runInTransaction(agent, (transaction) => { + headerAttributes.collectRequestHeaders(headers, transaction) + + const attributes = transaction.trace.attributes.get(DESTINATIONS.TRANS_TRACE) + assert.equal(attributes['request.headers.accept'], 'acceptValue') + assert.equal(attributes.Accept, undefined) + agent.config.allow_all_headers = true + end() + }) + }) + + await t.test('should strip `-` from headers', (t, end) => { + const { agent } = t.nr + const headers = { + 'content-type': 'valid-type' + } + + helper.runInTransaction(agent, (transaction) => { + headerAttributes.collectRequestHeaders(headers, transaction) + + const attributes = transaction.trace.attributes.get(DESTINATIONS.TRANS_TRACE) + assert.equal(attributes['request.headers.contentType'], 'valid-type') + assert.equal(attributes['content-type'], undefined) + end() + }) + }) + + await t.test('should lowercase first letter in headers', (t, end) => { + const { agent } = t.nr + const headers = { + 'Content-Type': 'valid-type' + } - t.test('#collectRequestHeaders', (t) => { - t.autoend() - t.beforeEach(beforeEach) - t.afterEach(afterEach) + helper.runInTransaction(agent, (transaction) => { + headerAttributes.collectRequestHeaders(headers, transaction) - t.test('should be case insensitive when allow_all_headers is false', (t) => { - const { agent } = t.context + const attributes = transaction.trace.attributes.get(DESTINATIONS.TRANS_TRACE) + assert.equal(attributes['request.headers.contentType'], 'valid-type') + assert.equal(attributes['Content-Type'], undefined) + assert.equal(attributes.ContentType, undefined) + end() + }) + }) + + await t.test('should capture a scrubbed version of the referer header', (t, end) => { + const { agent } = t.nr + const refererUrl = 'https://www.google.com/search/cats?scrubbed=false' + + const headers = { + referer: refererUrl + } + + helper.runInTransaction(agent, (transaction) => { + headerAttributes.collectRequestHeaders(headers, transaction) + + const attributes = transaction.trace.attributes.get(DESTINATIONS.TRANS_TRACE) + + assert.equal(attributes['request.headers.referer'], 'https://www.google.com/search/cats') + + end() + }) + }) + + await t.test( + 'with allow_all_headers set to false should only collect allowed agent-specified headers', + (t, end) => { + const { agent } = t.nr agent.config.allow_all_headers = false + const headers = { - Accept: 'acceptValue' + 'invalid': 'header', + 'referer': 'valid-referer', + 'content-type': 'valid-type' } helper.runInTransaction(agent, (transaction) => { headerAttributes.collectRequestHeaders(headers, transaction) const attributes = transaction.trace.attributes.get(DESTINATIONS.TRANS_TRACE) - t.equal(attributes['request.headers.accept'], 'acceptValue') - t.notOk(attributes.Accept) - agent.config.allow_all_headers = true - t.end() + assert.equal(attributes['request.headers.invalid'], undefined) + assert.equal(attributes['request.headers.referer'], 'valid-referer') + assert.equal(attributes['request.headers.contentType'], 'valid-type') + + end() }) - }) - t.test('should strip `-` from headers', (t) => { - const { agent } = t.context + } + ) + + await t.test( + 'with allow_all_headers set to false should collect allowed headers as span attributes', + (t, end) => { + const { agent } = t.nr + agent.config.allow_all_headers = false + const headers = { + 'invalid': 'header', + 'referer': 'valid-referer', 'content-type': 'valid-type' } @@ -68,182 +154,98 @@ tap.test('header-attributes', (t) => { headerAttributes.collectRequestHeaders(headers, transaction) const attributes = transaction.trace.attributes.get(DESTINATIONS.TRANS_TRACE) - t.equal(attributes['request.headers.contentType'], 'valid-type') - t.notOk(attributes['content-type']) - t.end() + assert.equal(attributes['request.headers.invalid'], undefined) + assert.equal(attributes['request.headers.referer'], 'valid-referer') + assert.equal(attributes['request.headers.contentType'], 'valid-type') + + const segment = transaction.agent.tracer.getSegment() + const spanAttributes = segment.attributes.get(DESTINATIONS.SPAN_EVENT) + + assert.equal(spanAttributes['request.headers.referer'], 'valid-referer') + assert.equal(spanAttributes['request.headers.contentType'], 'valid-type') + end() }) - }) + } + ) + + await t.test( + 'with allow_all_headers set to true should collect all headers not filtered by `exclude` rules', + (t, end) => { + const { agent } = t.nr + agent.config.allow_all_headers = true - t.test('should lowercase first letter in headers', (t) => { - const { agent } = t.context const headers = { - 'Content-Type': 'valid-type' + 'valid': 'header', + 'referer': 'valid-referer', + 'content-type': 'valid-type', + 'X-filtered-out': 'invalid' } helper.runInTransaction(agent, (transaction) => { headerAttributes.collectRequestHeaders(headers, transaction) const attributes = transaction.trace.attributes.get(DESTINATIONS.TRANS_TRACE) - t.equal(attributes['request.headers.contentType'], 'valid-type') - t.notOk(attributes['Content-Type']) - t.notOk(attributes.ContentType) - t.end() + assert.equal(attributes['request.headers.x-filtered-out'], undefined) + assert.equal(attributes['request.headers.xFilteredOut'], undefined) + assert.equal(attributes['request.headers.XFilteredOut'], undefined) + assert.equal(attributes['request.headers.valid'], 'header') + assert.equal(attributes['request.headers.referer'], 'valid-referer') + assert.equal(attributes['request.headers.contentType'], 'valid-type') + end() }) - }) + } + ) +}) - t.test('should capture a scrubbed version of the referer header', (t) => { - const { agent } = t.context - const refererUrl = 'https://www.google.com/search/cats?scrubbed=false' +test('#collectResponseHeaders', async (t) => { + t.beforeEach(beforeEach) + t.afterEach(afterEach) + + await t.test( + 'with allow_all_headers set to false should only collect allowed agent-specified headers', + (t, end) => { + const { agent } = t.nr + agent.config.allow_all_headers = false const headers = { - referer: refererUrl + 'invalid': 'header', + 'content-type': 'valid-type' } helper.runInTransaction(agent, (transaction) => { - headerAttributes.collectRequestHeaders(headers, transaction) + headerAttributes.collectResponseHeaders(headers, transaction) const attributes = transaction.trace.attributes.get(DESTINATIONS.TRANS_TRACE) - - t.equal(attributes['request.headers.referer'], 'https://www.google.com/search/cats') - - t.end() + assert.equal(attributes['response.headers.invalid'], undefined) + assert.equal(attributes['response.headers.contentType'], 'valid-type') + end() }) - }) - - t.test( - 'with allow_all_headers set to false should only collect allowed agent-specified headers', - (t) => { - const { agent } = t.context - agent.config.allow_all_headers = false - - const headers = { - 'invalid': 'header', - 'referer': 'valid-referer', - 'content-type': 'valid-type' - } - - helper.runInTransaction(agent, (transaction) => { - headerAttributes.collectRequestHeaders(headers, transaction) + } + ) - const attributes = transaction.trace.attributes.get(DESTINATIONS.TRANS_TRACE) - t.notOk(attributes['request.headers.invalid']) - t.equal(attributes['request.headers.referer'], 'valid-referer') - t.equal(attributes['request.headers.contentType'], 'valid-type') + await t.test( + 'with allow_all_headers set to true should collect all headers not filtered by `exclude` rules', + (t, end) => { + const { agent } = t.nr + agent.config.allow_all_headers = true - t.end() - }) - } - ) - - t.test( - 'with allow_all_headers set to false should collect allowed headers as span attributes', - (t) => { - const { agent } = t.context - agent.config.allow_all_headers = false - - const headers = { - 'invalid': 'header', - 'referer': 'valid-referer', - 'content-type': 'valid-type' - } - - helper.runInTransaction(agent, (transaction) => { - headerAttributes.collectRequestHeaders(headers, transaction) - - const attributes = transaction.trace.attributes.get(DESTINATIONS.TRANS_TRACE) - t.notOk(attributes['request.headers.invalid']) - t.equal(attributes['request.headers.referer'], 'valid-referer') - t.equal(attributes['request.headers.contentType'], 'valid-type') - - const segment = transaction.agent.tracer.getSegment() - const spanAttributes = segment.attributes.get(DESTINATIONS.SPAN_EVENT) - - t.equal(spanAttributes['request.headers.referer'], 'valid-referer') - t.equal(spanAttributes['request.headers.contentType'], 'valid-type') - t.end() - }) - } - ) - - t.test( - 'with allow_all_headers set to true should collect all headers not filtered by `exclude` rules', - (t) => { - const { agent } = t.context - agent.config.allow_all_headers = true - - const headers = { - 'valid': 'header', - 'referer': 'valid-referer', - 'content-type': 'valid-type', - 'X-filtered-out': 'invalid' - } - - helper.runInTransaction(agent, (transaction) => { - headerAttributes.collectRequestHeaders(headers, transaction) - - const attributes = transaction.trace.attributes.get(DESTINATIONS.TRANS_TRACE) - t.notOk(attributes['request.headers.x-filtered-out']) - t.notOk(attributes['request.headers.xFilteredOut']) - t.notOk(attributes['request.headers.XFilteredOut']) - t.equal(attributes['request.headers.valid'], 'header') - t.equal(attributes['request.headers.referer'], 'valid-referer') - t.equal(attributes['request.headers.contentType'], 'valid-type') - t.end() - }) + const headers = { + 'valid': 'header', + 'content-type': 'valid-type', + 'X-filtered-out': 'invalid' } - ) - }) - t.test('#collectResponseHeaders', (t) => { - t.autoend() - t.beforeEach(beforeEach) - t.afterEach(afterEach) - t.test( - 'with allow_all_headers set to false should only collect allowed agent-specified headers', - (t) => { - const { agent } = t.context - agent.config.allow_all_headers = false - - const headers = { - 'invalid': 'header', - 'content-type': 'valid-type' - } - - helper.runInTransaction(agent, (transaction) => { - headerAttributes.collectResponseHeaders(headers, transaction) - - const attributes = transaction.trace.attributes.get(DESTINATIONS.TRANS_TRACE) - t.notOk(attributes['response.headers.invalid']) - t.equal(attributes['response.headers.contentType'], 'valid-type') - t.end() - }) - } - ) - - t.test( - 'with allow_all_headers set to true should collect all headers not filtered by `exclude` rules', - (t) => { - const { agent } = t.context - agent.config.allow_all_headers = true - - const headers = { - 'valid': 'header', - 'content-type': 'valid-type', - 'X-filtered-out': 'invalid' - } - - helper.runInTransaction(agent, (transaction) => { - headerAttributes.collectResponseHeaders(headers, transaction) - - const attributes = transaction.trace.attributes.get(DESTINATIONS.TRANS_TRACE) - t.notOk(attributes['response.headers.x-filtered-out']) - t.notOk(attributes['response.headers.xFilteredOut']) - t.notOk(attributes['response.headers.XFilteredOut']) - t.equal(attributes['response.headers.valid'], 'header') - t.equal(attributes['response.headers.contentType'], 'valid-type') - t.end() - }) - } - ) - }) + helper.runInTransaction(agent, (transaction) => { + headerAttributes.collectResponseHeaders(headers, transaction) + + const attributes = transaction.trace.attributes.get(DESTINATIONS.TRANS_TRACE) + assert.equal(attributes['response.headers.x-filtered-out'], undefined) + assert.equal(attributes['response.headers.xFilteredOut'], undefined) + assert.equal(attributes['response.headers.XFilteredOut'], undefined) + assert.equal(attributes['response.headers.valid'], 'header') + assert.equal(attributes['response.headers.contentType'], 'valid-type') + end() + }) + } + ) }) diff --git a/test/unit/header-processing.test.js b/test/unit/header-processing.test.js index e0dbe9275e..fb343afcee 100644 --- a/test/unit/header-processing.test.js +++ b/test/unit/header-processing.test.js @@ -1,123 +1,118 @@ /* - * 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 headerProcessing = require('../../lib/header-processing') -tap.test('header-processing', (t) => { - t.test('#getContentLengthFromHeaders', (t) => { - t.test('should return content-length headers, case insensitive', (t) => { - // does it work? - t.equal(headerProcessing.getContentLengthFromHeaders({ 'Content-Length': 100 }), 100) - - // does it work with weird casing? - t.equal(headerProcessing.getContentLengthFromHeaders({ 'ConTent-LenGth': 100 }), 100) - - // does it ignore other headers? - t.equal( - headerProcessing.getContentLengthFromHeaders({ - 'zip': 'zap', - 'Content-Length': 100, - 'foo': 'bar' - }), - 100 - ) - - // when presented with two headers that are the same name - // but different case, does t.test prefer the first one found. - // This captures the exact behavior of the legacy code we're - // replacing - t.equal( - headerProcessing.getContentLengthFromHeaders({ - 'zip': 'zap', - 'content-length': 50, - 'Content-Length': 100, - 'foo': 'bar' - }), - 50 - ) - - // doesn't fail when working with null prototype objects - // (returned by res.getHeaders() is -- some? all? versions - // of NodeJS - const fixture = Object.create(null) - fixture.zip = 'zap' - fixture['content-length'] = 49 - fixture['Content-Length'] = 100 - fixture.foo = 'bar' - t.equal(headerProcessing.getContentLengthFromHeaders(fixture), 49) - t.end() - }) - - t.test('should return -1 if there is no header', (t) => { - t.equal(headerProcessing.getContentLengthFromHeaders({}), -1) - - t.equal(headerProcessing.getContentLengthFromHeaders('foo'), -1) - - t.equal(headerProcessing.getContentLengthFromHeaders([]), -1) - - t.equal(headerProcessing.getContentLengthFromHeaders({ foo: 'bar', zip: 'zap' }), -1) - t.end() - }) - t.end() - }) +const test = require('node:test') +const assert = require('node:assert') - t.test('#getQueueTime', (t) => { - // This header can hold up to 4096 bytes which could quickly fill up logs. - // Do not log a level higher than debug. - t.test('should not log invalid raw queue time higher than debug level', (t) => { - const invalidRawQueueTime = 'z1232442z' - const requestHeaders = { - 'x-queue-start': invalidRawQueueTime - } +const headerProcessing = require('../../lib/header-processing') - let didLogHighLevel = false - let didLogLowLevel = false +test('#getContentLengthFromHeaders', async (t) => { + await t.test('should return content-length headers, case insensitive', () => { + // does it work? + assert.equal(headerProcessing.getContentLengthFromHeaders({ 'Content-Length': 100 }), 100) + + // does it work with weird casing? + assert.equal(headerProcessing.getContentLengthFromHeaders({ 'ConTent-LenGth': 100 }), 100) + + // does it ignore other headers? + assert.equal( + headerProcessing.getContentLengthFromHeaders({ + 'zip': 'zap', + 'Content-Length': 100, + 'foo': 'bar' + }), + 100 + ) + + // when presented with two headers that are the same name + // but different case, does t.test prefer the first one found. + // This captures the exact behavior of the legacy code we're + // replacing + assert.equal( + headerProcessing.getContentLengthFromHeaders({ + 'zip': 'zap', + 'content-length': 50, + 'Content-Length': 100, + 'foo': 'bar' + }), + 50 + ) + + // doesn't fail when working with null prototype objects + // (returned by res.getHeaders() is -- some? all? versions + // of NodeJS + const fixture = Object.create(null) + fixture.zip = 'zap' + fixture['content-length'] = 49 + fixture['Content-Length'] = 100 + fixture.foo = 'bar' + assert.equal(headerProcessing.getContentLengthFromHeaders(fixture), 49) + }) - const mockLogger = { - trace: checkLogRawQueueTimeLowLevel, - debug: checkLogRawQueueTimeLowLevel, - info: checkLogRawQueueTimeHighLevel, - warn: checkLogRawQueueTimeHighLevel, - error: checkLogRawQueueTimeHighLevel - } + await t.test('should return -1 if there is no header', () => { + assert.equal(headerProcessing.getContentLengthFromHeaders({}), -1) - const queueTime = headerProcessing.getQueueTime(mockLogger, requestHeaders) + assert.equal(headerProcessing.getContentLengthFromHeaders('foo'), -1) - t.not(queueTime) - t.equal(didLogHighLevel, false) - t.equal(didLogLowLevel, true) - t.end() + assert.equal(headerProcessing.getContentLengthFromHeaders([]), -1) - function didLogRawQueueTime(args) { - let didLog = false + assert.equal(headerProcessing.getContentLengthFromHeaders({ foo: 'bar', zip: 'zap' }), -1) + }) +}) - args.forEach((argument) => { - const foundQueueTime = argument.indexOf(invalidRawQueueTime) >= 0 - if (foundQueueTime) { - didLog = true - } - }) +test('#getQueueTime', async (t) => { + // This header can hold up to 4096 bytes which could quickly fill up logs. + // Do not log a level higher than debug. + await t.test('should not log invalid raw queue time higher than debug level', () => { + const invalidRawQueueTime = 'z1232442z' + const requestHeaders = { + 'x-queue-start': invalidRawQueueTime + } + + let didLogHighLevel = false + let didLogLowLevel = false + + const mockLogger = { + trace: checkLogRawQueueTimeLowLevel, + debug: checkLogRawQueueTimeLowLevel, + info: checkLogRawQueueTimeHighLevel, + warn: checkLogRawQueueTimeHighLevel, + error: checkLogRawQueueTimeHighLevel + } + + const queueTime = headerProcessing.getQueueTime(mockLogger, requestHeaders) + + assert.equal(queueTime, undefined) + assert.equal(didLogHighLevel, false) + assert.equal(didLogLowLevel, true) + + function didLogRawQueueTime(args) { + let didLog = false + + args.forEach((argument) => { + const foundQueueTime = argument.indexOf(invalidRawQueueTime) >= 0 + if (foundQueueTime) { + didLog = true + } + }) - return didLog - } + return didLog + } - function checkLogRawQueueTimeHighLevel(...args) { - if (didLogRawQueueTime(args)) { - didLogHighLevel = true - } + function checkLogRawQueueTimeHighLevel(...args) { + if (didLogRawQueueTime(args)) { + didLogHighLevel = true } + } - function checkLogRawQueueTimeLowLevel(...args) { - if (didLogRawQueueTime(args)) { - didLogLowLevel = true - } + function checkLogRawQueueTimeLowLevel(...args) { + if (didLogRawQueueTime(args)) { + didLogLowLevel = true } - }) - t.end() + } }) - t.end() }) diff --git a/test/unit/high-security.test.js b/test/unit/high-security.test.js index df2f928993..2cd30c71d0 100644 --- a/test/unit/high-security.test.js +++ b/test/unit/high-security.test.js @@ -1,12 +1,14 @@ /* - * 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 { match } = require('../lib/custom-assertions') const helper = require('../lib/agent_helper') const facts = require('../../lib/collector/facts') const API = require('../../api') @@ -35,283 +37,254 @@ function getPath(obj, path) { return obj[paths[0]] } -tap.Test.prototype.addAssert('check', 3, function (key, before, after) { +function check(key, before, after) { const fromFile = { high_security: true } setPath(fromFile, key, before) const config = new Config(fromFile) - return this.same(getPath(config, key), after) -}) + return assert.equal(match(getPath(config, key), after), true) +} -tap.Test.prototype.addAssert('checkServer', 4, function (config, key, expected, server) { +function checkServer(config, key, expected, server) { setPath(config, key, expected) const fromServer = { high_security: true } fromServer[key] = server - this.same(getPath(config, key), expected) - this.same(fromServer[key], server) + assert.equal(match(getPath(config, key), expected), true) + assert.equal(match(fromServer[key], server), true) config.onConnect(fromServer) - return this.same(getPath(config, key), expected) + return assert.equal(match(getPath(config, key), expected), true) +} + +test('config to be sent during connect', async (t) => { + t.beforeEach((ctx) => { + ctx.nr = {} + ctx.nr.agent = helper.loadMockedAgent() + }) + + t.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) + }) + + await t.test('should contain high_security', async (t) => { + const { agent } = t.nr + const factoids = await new Promise((resolve) => { + facts(agent, resolve) + }) + assert.ok(Object.keys(factoids).includes('high_security')) + }) }) -tap.test('high security mode', function (t) { - t.autoend() +test('conditional application of server side settings', async (t) => { + await t.test('when high_security === true', async (t) => { + t.beforeEach((ctx) => { + ctx.nr = {} + ctx.nr.config = new Config({ high_security: true }) + }) - t.test('config to be sent during connect', function (t) { - t.autoend() - let agent = null + await t.test('should reject disabling ssl', (t) => { + const { config } = t.nr + checkServer(config, 'ssl', true, false) + }) - t.beforeEach(function () { - agent = helper.loadMockedAgent() + await t.test('should reject enabling allow_all_headers', (t) => { + const { config } = t.nr + checkServer(config, 'allow_all_headers', false, true) }) - t.afterEach(function () { - helper.unloadAgent(agent) + await t.test('should reject enabling slow_sql', (t) => { + const { config } = t.nr + checkServer(config, 'slow_sql.enabled', false, true) }) - t.test('should contain high_security', async function (t) { - const factoids = await new Promise((resolve) => { - facts(agent, resolve) - }) - t.ok(Object.keys(factoids).includes('high_security')) + await t.test('should not change attributes settings', (t) => { + const { config } = t.nr + checkServer(config, 'attributes.include', [], ['foobar']) + checkServer(config, 'attributes.exclude', [], ['fizzbang', 'request.parameters.*']) }) - }) - t.test('conditional application of server side settings', function (t) { - t.autoend() - let config = null - - t.test('when high_security === true', function (t) { - t.autoend() - - t.beforeEach(function () { - config = new Config({ high_security: true }) - }) - - t.test('should reject disabling ssl', function (t) { - t.checkServer(config, 'ssl', true, false) - t.end() - }) - - t.test('should reject enabling allow_all_headers', function (t) { - t.checkServer(config, 'allow_all_headers', false, true) - t.end() - }) - - t.test('should reject enabling slow_sql', function (t) { - t.checkServer(config, 'slow_sql.enabled', false, true) - t.end() - }) - - t.test('should not change attributes settings', function (t) { - t.checkServer(config, 'attributes.include', [], ['foobar']) - t.checkServer(config, 'attributes.exclude', [], ['fizzbang', 'request.parameters.*']) - t.end() - }) - - t.test('should not change transaction_tracer settings', function (t) { - t.checkServer(config, 'transaction_tracer.record_sql', 'obfuscated', 'raw') - t.checkServer(config, 'transaction_tracer.attributes.include', [], ['foobar']) - t.checkServer(config, 'transaction_tracer.attributes.exclude', [], ['fizzbang']) - t.end() - }) - - t.test('should not change error_collector settings', function (t) { - t.checkServer(config, 'error_collector.attributes.include', [], ['foobar']) - t.checkServer(config, 'error_collector.attributes.exclude', [], ['fizzbang']) - t.end() - }) - - t.test('should not change browser_monitoring settings', function (t) { - t.checkServer(config, 'browser_monitoring.attributes.include', [], ['foobar']) - t.checkServer(config, 'browser_monitoring.attributes.exclude', [], ['fizzbang']) - t.end() - }) - - t.test('should not change transaction_events settings', function (t) { - t.checkServer(config, 'transaction_events.attributes.include', [], ['foobar']) - t.checkServer(config, 'transaction_events.attributes.exclude', [], ['fizzbang']) - t.end() - }) - - t.test('should shut down the agent if high_security is false', function (t) { - config.onConnect({ high_security: false }) - t.equal(config.agent_enabled, false) - t.end() - }) - - t.test('should shut down the agent if high_security is missing', function (t) { - config.onConnect({}) - t.equal(config.agent_enabled, false) - t.end() - }) - - t.test('should disable application logging forwarding', (t) => { - t.checkServer(config, 'application_logging.forwarding.enabled', false, true) - t.end() - }) + await t.test('should not change transaction_tracer settings', (t) => { + const { config } = t.nr + checkServer(config, 'transaction_tracer.record_sql', 'obfuscated', 'raw') + checkServer(config, 'transaction_tracer.attributes.include', [], ['foobar']) + checkServer(config, 'transaction_tracer.attributes.exclude', [], ['fizzbang']) }) - t.test('when high_security === false', function (t) { - t.autoend() + await t.test('should not change error_collector settings', (t) => { + const { config } = t.nr + checkServer(config, 'error_collector.attributes.include', [], ['foobar']) + checkServer(config, 'error_collector.attributes.exclude', [], ['fizzbang']) + }) - t.beforeEach(function () { - config = new Config({ high_security: false }) - }) + await t.test('should not change browser_monitoring settings', (t) => { + const { config } = t.nr + checkServer(config, 'browser_monitoring.attributes.include', [], ['foobar']) + checkServer(config, 'browser_monitoring.attributes.exclude', [], ['fizzbang']) + }) - t.test('should accept disabling ssl', function (t) { - // enabled by defualt, but lets make sure. - config.ssl = true - config.onConnect({ ssl: false }) - t.equal(config.ssl, true) - t.end() - }) + await t.test('should not change transaction_events settings', (t) => { + const { config } = t.nr + checkServer(config, 'transaction_events.attributes.include', [], ['foobar']) + checkServer(config, 'transaction_events.attributes.exclude', [], ['fizzbang']) + }) + + await t.test('should shut down the agent if high_security is false', (t) => { + const { config } = t.nr + config.onConnect({ high_security: false }) + assert.equal(config.agent_enabled, false) + }) + + await t.test('should shut down the agent if high_security is missing', (t) => { + const { config } = t.nr + config.onConnect({}) + assert.equal(config.agent_enabled, false) + }) + + await t.test('should disable application logging forwarding', (t) => { + const { config } = t.nr + checkServer(config, 'application_logging.forwarding.enabled', false, true) }) }) - t.test('coerces other settings', function (t) { - t.autoend() - - t.test('_applyHighSecurity during init', function (t) { - t.autoend() - - const orig = Config.prototype._applyHighSecurity - let called - - t.beforeEach(function () { - called = false - Config.prototype._applyHighSecurity = function () { - called = true - } - }) - - t.afterEach(function () { - Config.prototype._applyHighSecurity = orig - }) - - t.test('should call if high_security is on', function (t) { - new Config({ high_security: true }) // eslint-disable-line no-new - t.equal(called, true) - t.end() - }) - - t.test('should not call if high_security is off', function (t) { - new Config({ high_security: false }) // eslint-disable-line no-new - t.equal(called, false) - t.end() - }) + await t.test('when high_security === false', async (t) => { + t.beforeEach((ctx) => { + ctx.nr = {} + ctx.nr.config = new Config({ high_security: false }) }) - t.test('when high_security === true', function (t) { - t.autoend() - - t.test('should detect that ssl is off', function (t) { - t.check('ssl', false, true) - t.end() - }) - - t.test('should detect that allow_all_headers is on', function (t) { - t.check('allow_all_headers', true, false) - t.end() - }) - - t.test('should change attributes settings', function (t) { - // Should not touch `enabled` setting or exclude. - t.check('attributes.enabled', true, true) - t.check('attributes.enabled', false, false) - t.check('attributes.exclude', ['fizbang'], ['fizbang', 'request.parameters.*']) - t.check('attributes.include', ['foobar'], []) - t.end() - }) - - t.test('should change transaction_tracer settings', function (t) { - t.check('transaction_tracer.record_sql', 'raw', 'obfuscated') - - // Should not touch `enabled` setting. - t.check('transaction_tracer.attributes.enabled', true, true) - t.check('transaction_tracer.attributes.enabled', false, false) - - t.check('transaction_tracer.attributes.include', ['foobar'], []) - t.check('transaction_tracer.attributes.exclude', ['fizbang'], ['fizbang']) - t.end() - }) - - t.test('should change error_collector settings', function (t) { - // Should not touch `enabled` setting. - t.check('error_collector.attributes.enabled', true, true) - t.check('error_collector.attributes.enabled', false, false) - - t.check('error_collector.attributes.include', ['foobar'], []) - t.check('error_collector.attributes.exclude', ['fizbang'], ['fizbang']) - t.end() - }) - - t.test('should change browser_monitoring settings', function (t) { - // Should not touch `enabled` setting. - t.check('browser_monitoring.attributes.enabled', true, true) - t.check('browser_monitoring.attributes.enabled', false, false) - - t.check('browser_monitoring.attributes.include', ['foobar'], []) - t.check('browser_monitoring.attributes.exclude', ['fizbang'], ['fizbang']) - t.end() - }) - - t.test('should change transaction_events settings', function (t) { - // Should not touch `enabled` setting. - t.check('transaction_events.attributes.enabled', true, true) - t.check('transaction_events.attributes.enabled', false, false) - - t.check('transaction_events.attributes.include', ['foobar'], []) - t.check('transaction_events.attributes.exclude', ['fizbang'], ['fizbang']) - t.end() - }) - - t.test('should detect that slow_sql is enabled', function (t) { - t.check('slow_sql.enabled', true, false) - t.end() - }) - - t.test('should detect no problems', function (t) { - const config = new Config({ high_security: true }) - config.ssl = true - config.attributes.include = ['some val'] - config._applyHighSecurity() - t.equal(config.ssl, true) - t.same(config.attributes.include, []) - t.end() - }) + await t.test('should accept disabling ssl', (t) => { + const { config } = t.nr + // enabled by default, but lets make sure. + config.ssl = true + config.onConnect({ ssl: false }) + assert.equal(config.ssl, true) }) }) +}) - t.test('affect custom params', function (t) { - t.autoend() - let agent = null - let api = null +test('coerces other settings', async (t) => { + await t.test('coerces other settings', async (t) => { + t.beforeEach((ctx) => { + ctx.nr = {} - t.beforeEach(function () { - agent = helper.loadMockedAgent() - api = new API(agent) + ctx.nr.orig = Config.prototype._applyHighSecurity + ctx.nr.called = false + Config.prototype._applyHighSecurity = () => { + ctx.nr.called = true + } }) - t.afterEach(function () { - helper.unloadAgent(agent) + t.afterEach((ctx) => { + Config.prototype._applyHighSecurity = ctx.nr.orig }) - t.test('should disable addCustomAttribute if high_security is on', function (t) { - agent.config.high_security = true - const success = api.addCustomAttribute('key', 'value') - t.equal(success, false) - t.end() + await t.test('should call if high_security is on', (t) => { + new Config({ high_security: true }) // eslint-disable-line no-new + assert.equal(t.nr.called, true) + }) + + await t.test('should not call if high_security is off', (t) => { + new Config({ high_security: false }) // eslint-disable-line no-new + assert.equal(t.nr.called, false) + }) + }) + + await t.test('when high_security === true', async (t) => { + await t.test('should detect that ssl is off', () => { + check('ssl', false, true) + }) + + await t.test('should detect that allow_all_headers is on', () => { + check('allow_all_headers', true, false) + }) + + await t.test('should change attributes settings', () => { + // Should not touch `enabled` setting or exclude. + check('attributes.enabled', true, true) + check('attributes.enabled', false, false) + check('attributes.exclude', ['fizbang'], ['fizbang', 'request.parameters.*']) + check('attributes.include', ['foobar'], []) + }) + + await t.test('should change transaction_tracer settings', () => { + check('transaction_tracer.record_sql', 'raw', 'obfuscated') + + // Should not touch `enabled` setting. + check('transaction_tracer.attributes.enabled', true, true) + check('transaction_tracer.attributes.enabled', false, false) + + check('transaction_tracer.attributes.include', ['foobar'], []) + check('transaction_tracer.attributes.exclude', ['fizbang'], ['fizbang']) }) - t.test('should not affect addCustomAttribute if high_security is off', function (t) { - helper.runInTransaction(agent, () => { - agent.config.high_security = false - const success = api.addCustomAttribute('key', 'value') - t.notOk(success) - t.end() - }) + await t.test('should change error_collector settings', () => { + // Should not touch `enabled` setting. + check('error_collector.attributes.enabled', true, true) + check('error_collector.attributes.enabled', false, false) + + check('error_collector.attributes.include', ['foobar'], []) + check('error_collector.attributes.exclude', ['fizbang'], ['fizbang']) + }) + + await t.test('should change browser_monitoring settings', () => { + // Should not touch `enabled` setting. + check('browser_monitoring.attributes.enabled', true, true) + check('browser_monitoring.attributes.enabled', false, false) + + check('browser_monitoring.attributes.include', ['foobar'], []) + check('browser_monitoring.attributes.exclude', ['fizbang'], ['fizbang']) + }) + + await t.test('should change transaction_events settings', () => { + // Should not touch `enabled` setting. + check('transaction_events.attributes.enabled', true, true) + check('transaction_events.attributes.enabled', false, false) + + check('transaction_events.attributes.include', ['foobar'], []) + check('transaction_events.attributes.exclude', ['fizbang'], ['fizbang']) + }) + + await t.test('should detect that slow_sql is enabled', () => { + check('slow_sql.enabled', true, false) + }) + + await t.test('should detect no problems', () => { + const config = new Config({ high_security: true }) + config.ssl = true + config.attributes.include = ['some val'] + config._applyHighSecurity() + assert.equal(config.ssl, true) + assert.equal(match(config.attributes.include, []), true) + }) + }) +}) + +test('affect custom params', async (t) => { + t.beforeEach((ctx) => { + ctx.nr = {} + ctx.nr.agent = helper.loadMockedAgent() + ctx.nr.api = new API(ctx.nr.agent) + }) + + t.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) + }) + + await t.test('should disable addCustomAttribute if high_security is on', (t) => { + const { agent, api } = t.nr + agent.config.high_security = true + const success = api.addCustomAttribute('key', 'value') + assert.equal(success, false) + }) + + await t.test('should not affect addCustomAttribute if high_security is off', (t, end) => { + const { agent, api } = t.nr + helper.runInTransaction(agent, () => { + agent.config.high_security = false + const success = api.addCustomAttribute('key', 'value') + assert.equal(success, undefined) + end() }) }) }) diff --git a/test/unit/util/hashes.test.js b/test/unit/util/hashes.test.js index fec849b74c..79f59590be 100644 --- a/test/unit/util/hashes.test.js +++ b/test/unit/util/hashes.test.js @@ -4,33 +4,69 @@ */ 'use strict' + const assert = require('node:assert') const test = require('node:test') + +const testData = require('../../lib/obfuscation-data') const hashes = require('../../../lib/util/hashes') -test('hashes', async (t) => { - await t.test('#makeId always returns the correct length', () => { - for (let length = 4; length < 64; length++) { - for (let attempts = 0; attempts < 500; attempts++) { - const id = hashes.makeId(length) - assert.equal(id.length, length) - } +const major = process.version.slice(1).split('.').map(Number).shift() + +test('#makeId always returns the correct length', () => { + for (let length = 4; length < 64; length++) { + for (let attempts = 0; attempts < 500; attempts++) { + const id = hashes.makeId(length) + assert.equal(id.length, length) } - }) + } +}) + +test('#makeId always unique', () => { + const ids = {} + for (let length = 16; length < 64; length++) { + for (let attempts = 0; attempts < 500; attempts++) { + const id = hashes.makeId(length) - await t.test('#makeId always unique', () => { - const ids = {} - for (let length = 16; length < 64; length++) { - for (let attempts = 0; attempts < 500; attempts++) { - const id = hashes.makeId(length) + // Should be unique + assert.equal(ids[id], undefined) + ids[id] = true - // Should be unique - assert.equal(ids[id], undefined) - ids[id] = true + // and the correct length + assert.equal(id.length, length) + } + } +}) - // and the correct length - assert.equal(id.length, length) - } +test('obfuscation', async (t) => { + await t.test('should obfuscate strings correctly', () => { + for (const data of testData) { + assert.equal(hashes.obfuscateNameUsingKey(data.input, data.key), data.output) } }) }) + +test('deobfuscation', async (t) => { + await t.test('should deobfuscate strings correctly', () => { + for (const data of testData) { + assert.equal(hashes.deobfuscateNameUsingKey(data.output, data.key), data.input) + } + }) +}) + +// TODO: remove this test when we drop support for node 18 +test('getHash', { skip: major > 18 }, async (t) => { + /** + * TODO: crypto.DEFAULT_ENCODING has been deprecated. + * When fully disabled, this test can likely be removed. + * https://nodejs.org/api/deprecations.html#DEP0091 + */ + /* eslint-disable node/no-deprecated-api */ + await t.test('should not crash when changing the DEFAULT_ENCODING key on crypto', () => { + const crypto = require('node:crypto') + const oldEncoding = crypto.DEFAULT_ENCODING + crypto.DEFAULT_ENCODING = 'utf-8' + assert.doesNotThrow(hashes.getHash.bind(null, 'TEST_APP', 'TEST_TXN')) + crypto.DEFAULT_ENCODING = oldEncoding + }) +})