From 8564705179b864bb055be2c722fb8995ac96a319 Mon Sep 17 00:00:00 2001 From: James Sumners Date: Wed, 7 Aug 2024 08:44:15 -0400 Subject: [PATCH] chore: Fixed mongodb-esm tests in combination with security agent --- test/versioned/mongodb-esm/bulk.tap.mjs | 80 --- test/versioned/mongodb-esm/bulk.test.mjs | 79 +++ .../mongodb-esm/collection-common.mjs | 210 ------- .../mongodb-esm/collection-find.tap.mjs | 85 --- .../mongodb-esm/collection-find.test.mjs | 88 +++ .../mongodb-esm/collection-index.tap.mjs | 91 ---- .../mongodb-esm/collection-index.test.mjs | 108 ++++ .../mongodb-esm/collection-misc.tap.mjs | 170 ------ .../mongodb-esm/collection-misc.test.mjs | 218 ++++++++ .../mongodb-esm/collection-update.tap.mjs | 212 -------- .../mongodb-esm/collection-update.test.mjs | 225 ++++++++ test/versioned/mongodb-esm/cursor.tap.mjs | 60 -- test/versioned/mongodb-esm/cursor.test.mjs | 83 +++ test/versioned/mongodb-esm/db.tap.mjs | 323 ----------- test/versioned/mongodb-esm/db.test.mjs | 513 ++++++++++++++++++ test/versioned/mongodb-esm/package.json | 17 +- .../versioned/mongodb-esm/test-assertions.mjs | 163 ++++++ test/versioned/mongodb-esm/test-hooks.mjs | 75 +++ test/versioned/mongodb/common.js | 3 +- 19 files changed, 1562 insertions(+), 1241 deletions(-) delete mode 100644 test/versioned/mongodb-esm/bulk.tap.mjs create mode 100644 test/versioned/mongodb-esm/bulk.test.mjs delete mode 100644 test/versioned/mongodb-esm/collection-common.mjs delete mode 100644 test/versioned/mongodb-esm/collection-find.tap.mjs create mode 100644 test/versioned/mongodb-esm/collection-find.test.mjs delete mode 100644 test/versioned/mongodb-esm/collection-index.tap.mjs create mode 100644 test/versioned/mongodb-esm/collection-index.test.mjs delete mode 100644 test/versioned/mongodb-esm/collection-misc.tap.mjs create mode 100644 test/versioned/mongodb-esm/collection-misc.test.mjs delete mode 100644 test/versioned/mongodb-esm/collection-update.tap.mjs create mode 100644 test/versioned/mongodb-esm/collection-update.test.mjs delete mode 100644 test/versioned/mongodb-esm/cursor.tap.mjs create mode 100644 test/versioned/mongodb-esm/cursor.test.mjs delete mode 100644 test/versioned/mongodb-esm/db.tap.mjs create mode 100644 test/versioned/mongodb-esm/db.test.mjs create mode 100644 test/versioned/mongodb-esm/test-assertions.mjs create mode 100644 test/versioned/mongodb-esm/test-hooks.mjs diff --git a/test/versioned/mongodb-esm/bulk.tap.mjs b/test/versioned/mongodb-esm/bulk.tap.mjs deleted file mode 100644 index e01f75a30d..0000000000 --- a/test/versioned/mongodb-esm/bulk.tap.mjs +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2022 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import tap from 'tap' -import { test } from './collection-common.mjs' -import helper from '../../lib/agent_helper.js' -import { ESM } from './common.cjs' -const { STATEMENT_PREFIX } = ESM - -tap.test('Bulk operations', (t) => { - t.autoend() - let agent - - t.before(() => { - agent = helper.instrumentMockedAgent() - }) - - t.teardown(() => { - helper.unloadAgent(agent) - }) - - test( - { suiteName: 'unorderedBulkOp', agent, t }, - function unorderedBulkOpTest(t, collection, verify) { - const bulk = collection.initializeUnorderedBulkOp() - bulk - .find({ - i: 1 - }) - .updateOne({ - $set: { foo: 'bar' } - }) - bulk - .find({ - i: 2 - }) - .updateOne({ - $set: { foo: 'bar' } - }) - - bulk.execute(function done(err) { - t.error(err) - verify( - null, - [`${STATEMENT_PREFIX}/unorderedBulk/batch`, 'Callback: done'], - ['unorderedBulk'] - ) - }) - } - ) - - test( - { suiteName: 'orderedBulkOp', agent, t }, - function unorderedBulkOpTest(t, collection, verify) { - const bulk = collection.initializeOrderedBulkOp() - bulk - .find({ - i: 1 - }) - .updateOne({ - $set: { foo: 'bar' } - }) - - bulk - .find({ - i: 2 - }) - .updateOne({ - $set: { foo: 'bar' } - }) - - bulk.execute(function done(err) { - t.error(err) - verify(null, [`${STATEMENT_PREFIX}/orderedBulk/batch`, 'Callback: done'], ['orderedBulk']) - }) - } - ) -}) diff --git a/test/versioned/mongodb-esm/bulk.test.mjs b/test/versioned/mongodb-esm/bulk.test.mjs new file mode 100644 index 0000000000..1bd0318cf0 --- /dev/null +++ b/test/versioned/mongodb-esm/bulk.test.mjs @@ -0,0 +1,79 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import test from 'node:test' +import assert from 'node:assert' +import helper from '../../lib/agent_helper.js' +import common from '../mongodb/common.js' +import { beforeEach, afterEach } from './test-hooks.mjs' +import { getValidatorCallback } from './test-assertions.mjs' + +const { + ESM: { STATEMENT_PREFIX } +} = common + +test('unordered bulk operations', async (t) => { + t.beforeEach(beforeEach) + t.afterEach(afterEach) + + await t.test('should generate the correct metrics and segments', (t, end) => { + const { agent, collection } = t.nr + const segments = [`${STATEMENT_PREFIX}/unorderedBulk/batch`, 'Callback: done'] + const metrics = ['unorderedBulk'] + + helper.runInTransaction(agent, (tx) => { + tx.name = common.TRANSACTION_NAME + const bulk = collection.initializeUnorderedBulkOp() + bulk.find({ i: 1 }).updateOne({ $set: { foo: 'bar' } }) + bulk.find({ i: 2 }).updateOne({ $set: { foo: 'bar' } }) + bulk.execute(getValidatorCallback({ t, tx, metrics, segments, end })) + }) + }) + + await t.test('should not error outside of a transaction', (t, end) => { + const { agent, collection } = t.nr + assert.equal(agent.getTransaction(), undefined, 'should not be in a transaction') + const bulk = collection.initializeUnorderedBulkOp() + bulk.find({ i: 1 }).updateOne({ $set: { foo: 'bar' } }) + bulk.find({ i: 2 }).updateOne({ $set: { foo: 'bar' } }) + bulk.execute(function done(error) { + assert.equal(error, undefined, 'running test should not error') + assert.equal(agent.getTransaction(), undefined, 'should not somehow gain a transaction') + end() + }) + }) +}) + +test('ordered bulk operations', async (t) => { + t.beforeEach(beforeEach) + t.afterEach(afterEach) + + await t.test('should generate the correct metrics and segments', (t, end) => { + const { agent, collection } = t.nr + const segments = [`${STATEMENT_PREFIX}/orderedBulk/batch`, 'Callback: done'] + const metrics = ['orderedBulk'] + + helper.runInTransaction(agent, (tx) => { + tx.name = common.TRANSACTION_NAME + const bulk = collection.initializeOrderedBulkOp() + bulk.find({ i: 1 }).updateOne({ $set: { foo: 'bar' } }) + bulk.find({ i: 2 }).updateOne({ $set: { foo: 'bar' } }) + bulk.execute(getValidatorCallback({ t, tx, metrics, segments, end })) + }) + }) + + await t.test('should not error outside of a transaction', (t, end) => { + const { agent, collection } = t.nr + assert.equal(agent.getTransaction(), undefined, 'should not be in a transaction') + const bulk = collection.initializeOrderedBulkOp() + bulk.find({ i: 1 }).updateOne({ $set: { foo: 'bar' } }) + bulk.find({ i: 2 }).updateOne({ $set: { foo: 'bar' } }) + bulk.execute(function done(error) { + assert.equal(error, undefined, 'running test should not error') + assert.equal(agent.getTransaction(), undefined, 'should not somehow gain a transaction') + end() + }) + }) +}) diff --git a/test/versioned/mongodb-esm/collection-common.mjs b/test/versioned/mongodb-esm/collection-common.mjs deleted file mode 100644 index 49a369fa3a..0000000000 --- a/test/versioned/mongodb-esm/collection-common.mjs +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import common from './common.cjs' -import helper from '../../lib/agent_helper.js' - -let METRIC_HOST_NAME = null -let METRIC_HOST_PORT = null - -const MONGO_SEGMENT_RE = common.MONGO_SEGMENT_RE -const TRANSACTION_NAME = common.TRANSACTION_NAME -const { - ESM: { DB_NAME, STATEMENT_PREFIX, COLLECTIONS } -} = common -const { connect, close } = common - -export { - MONGO_SEGMENT_RE, - TRANSACTION_NAME, - DB_NAME, - connect, - close, - populate, - test, - dropTestCollections -} - -function test({ suiteName, agent, t }, run) { - t.test(suiteName, { timeout: 10000 }, function (t) { - let client = null - let db = null - let collection = null - t.autoend() - - t.beforeEach(async function () { - const { default: mongodb } = await import('mongodb') - return dropTestCollections(mongodb) - .then(() => { - METRIC_HOST_NAME = common.getHostName(agent) - METRIC_HOST_PORT = common.getPort() - return common.connect({ mongodb, name: DB_NAME }) - }) - .then((res) => { - client = res.client - db = res.db - collection = db.collection(COLLECTIONS.collection1) - return populate(db, collection) - }) - }) - - t.afterEach(function () { - // since we do not bootstrap a new agent between tests - // metrics will leak across runs if we do not clear - agent.metrics.clear() - return common.close(client, db) - }) - - t.test('should not error outside of a transaction', function (t) { - t.notOk(agent.getTransaction(), 'should not be in a transaction') - run(t, collection, function (err) { - t.error(err, 'running test should not error') - t.notOk(agent.getTransaction(), 'should not somehow gain a transaction') - t.end() - }) - }) - - t.test('should generate the correct metrics and segments', function (t) { - helper.runInTransaction(agent, function (transaction) { - transaction.name = common.TRANSACTION_NAME - run( - t, - collection, - function (err, segments, metrics, { childrenLength = 1, strict = true } = {}) { - if ( - !t.error(err, 'running test should not error') || - !t.ok(agent.getTransaction(), 'should maintain tx state') - ) { - return t.end() - } - t.equal(agent.getTransaction().id, transaction.id, 'should not change transactions') - const segment = agent.tracer.getSegment() - let current = transaction.trace.root - - // this logic is just for the collection.aggregate. - // aggregate no longer returns a callback with cursor - // it just returns a cursor. so the segments on the - // transaction are not nested but both on the trace - // root. instead of traversing the children, just - // iterate over the expected segments and compare - // against the corresponding child on trace root - // we also added a strict flag for aggregate because depending on the version - // there is an extra segment for the callback of our test which we do not care - // to assert - if (childrenLength === 2) { - t.equal(current.children.length, childrenLength, 'should have one child') - - segments.forEach((expectedSegment, i) => { - const child = current.children[i] - - t.equal(child.name, expectedSegment, `child should be named ${expectedSegment}`) - if (common.MONGO_SEGMENT_RE.test(child.name)) { - checkSegmentParams(t, child) - t.equal(child.ignore, false, 'should not ignore segment') - } - - if (strict) { - t.equal(child.children.length, 0, 'should have no more children') - } - }) - } else { - for (let i = 0, l = segments.length; i < l; ++i) { - t.equal(current.children.length, childrenLength, 'should have one child') - current = current.children[0] - t.equal(current.name, segments[i], 'child should be named ' + segments[i]) - if (common.MONGO_SEGMENT_RE.test(current.name)) { - checkSegmentParams(t, current) - t.equal(current.ignore, false, 'should not ignore segment') - } - } - - if (strict) { - t.equal(current.children.length, 0, 'should have no more children') - } - } - - if (strict) { - t.ok(current === segment, 'should test to the current segment') - } - - transaction.end() - common.checkMetrics({ - t, - agent, - host: METRIC_HOST_NAME, - port: METRIC_HOST_PORT, - metrics, - prefix: STATEMENT_PREFIX - }) - t.end() - } - ) - }) - }) - }) -} - -function checkSegmentParams(t, segment) { - let dbName = DB_NAME - if (/\/rename$/.test(segment.name)) { - dbName = 'admin' - } - - const attributes = segment.getAttributes() - t.equal(attributes.database_name, dbName, 'should have correct db name') - t.equal(attributes.host, METRIC_HOST_NAME, 'should have correct host name') - t.equal(attributes.port_path_or_id, METRIC_HOST_PORT, 'should have correct port') -} - -function populate(db, collection) { - return new Promise((resolve, reject) => { - const items = [] - for (let i = 0; i < 30; ++i) { - items.push({ - i: i, - next3: [i + 1, i + 2, i + 3], - data: Math.random().toString(36).slice(2), - mod10: i % 10, - // spiral out - loc: [i % 4 && (i + 1) % 4 ? i : -i, (i + 1) % 4 && (i + 2) % 4 ? i : -i] - }) - } - - db.collection(COLLECTIONS.collection2).drop(function () { - collection.deleteMany({}, function (err) { - if (err) { - reject(err) - } - collection.insert(items, resolve) - }) - }) - }) -} - -/** - * Bootstrap a running MongoDB instance by dropping all the collections used - * by tests. - * @param {*} mongodb MongoDB module to execute commands on. - */ -async function dropTestCollections(mongodb) { - const collections = Object.values(COLLECTIONS) - const { client, db } = await common.connect({ mongodb, name: DB_NAME }) - - const dropCollectionPromises = collections.map(async (collection) => { - try { - await db.dropCollection(collection) - } catch (err) { - if (err && err.errmsg !== 'ns not found') { - throw err - } - } - }) - - try { - await Promise.all(dropCollectionPromises) - } finally { - await common.close(client, db) - } -} diff --git a/test/versioned/mongodb-esm/collection-find.tap.mjs b/test/versioned/mongodb-esm/collection-find.tap.mjs deleted file mode 100644 index 6b0dd0f61a..0000000000 --- a/test/versioned/mongodb-esm/collection-find.tap.mjs +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import tap from 'tap' -import { test } from './collection-common.mjs' -import helper from '../../lib/agent_helper.js' -import { ESM } from './common.cjs' -const { STATEMENT_PREFIX } = ESM - -const findOpt = { returnDocument: 'after' } - -tap.test('Collection(Find) Tests', (t) => { - t.autoend() - let agent - - t.before(() => { - agent = helper.instrumentMockedAgent() - }) - - t.teardown(() => { - helper.unloadAgent(agent) - }) - - test({ suiteName: 'findOne', agent, t }, function findOneTest(t, collection, verify) { - collection.findOne({ i: 15 }, function done(err, data) { - t.error(err) - t.equal(data.i, 15) - verify(null, [`${STATEMENT_PREFIX}/findOne`, 'Callback: done'], ['findOne']) - }) - }) - - test( - { suiteName: 'findOneAndDelete', agent, t }, - function findOneAndDeleteTest(t, collection, verify) { - collection.findOneAndDelete({ i: 15 }, function done(err, data) { - t.error(err) - t.equal(data.ok, 1) - t.equal(data.value.i, 15) - verify( - null, - [`${STATEMENT_PREFIX}/findOneAndDelete`, 'Callback: done'], - ['findOneAndDelete'] - ) - }) - } - ) - - test( - { suiteName: 'findOneAndReplace', agent, t }, - function findAndReplaceTest(t, collection, verify) { - collection.findOneAndReplace({ i: 15 }, { b: 15 }, findOpt, done) - - function done(err, data) { - t.error(err) - t.equal(data.value.b, 15) - t.equal(data.ok, 1) - verify( - null, - [`${STATEMENT_PREFIX}/findOneAndReplace`, 'Callback: done'], - ['findOneAndReplace'] - ) - } - } - ) - - test( - { suiteName: 'findOneAndUpdate', agent, t }, - function findOneAndUpdateTest(t, collection, verify) { - collection.findOneAndUpdate({ i: 15 }, { $set: { a: 15 } }, findOpt, done) - - function done(err, data) { - t.error(err) - t.equal(data.value.a, 15) - t.equal(data.ok, 1) - verify( - null, - [`${STATEMENT_PREFIX}/findOneAndUpdate`, 'Callback: done'], - ['findOneAndUpdate'] - ) - } - } - ) -}) diff --git a/test/versioned/mongodb-esm/collection-find.test.mjs b/test/versioned/mongodb-esm/collection-find.test.mjs new file mode 100644 index 0000000000..13c014f364 --- /dev/null +++ b/test/versioned/mongodb-esm/collection-find.test.mjs @@ -0,0 +1,88 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import test from 'node:test' +import assert from 'node:assert' +import helper from '../../lib/agent_helper.js' +import { ESM } from './common.cjs' +import { beforeEach, afterEach } from './test-hooks.mjs' +import { getValidatorCallback } from './test-assertions.mjs' +import common from '../mongodb/common.js' + +const { STATEMENT_PREFIX } = ESM +const findOpt = { returnDocument: 'after' } + +test('collection find tests', async (t) => { + t.beforeEach(beforeEach) + t.afterEach(afterEach) + + await t.test('findOne', (t, end) => { + const { agent, collection } = t.nr + const segments = [`${STATEMENT_PREFIX}/findOne`, 'Callback: done'] + const metrics = ['findOne'] + + helper.runInTransaction(agent, (tx) => { + tx.name = common.TRANSACTION_NAME + collection.findOne({ i: 15 }, function done(error, data) { + assert.equal(error, undefined) + assert.equal(data.i, 15) + getValidatorCallback({ t, tx, metrics, segments, end })() + }) + }) + }) + + await t.test('findOneAndDelete', (t, end) => { + const { agent, collection } = t.nr + const segments = [`${STATEMENT_PREFIX}/findOneAndDelete`, 'Callback: done'] + const metrics = ['findOneAndDelete'] + + helper.runInTransaction(agent, (tx) => { + tx.name = common.TRANSACTION_NAME + collection.findOneAndDelete({ i: 15 }, function done(error, data) { + assert.equal(error, undefined) + assert.equal(data.ok, 1) + assert.equal(data.value.i, 15) + getValidatorCallback({ t, tx, metrics, segments, end })() + }) + }) + }) + + await t.test('findOneAndReplace', (t, end) => { + const { agent, collection } = t.nr + const segments = [`${STATEMENT_PREFIX}/findOneAndReplace`, 'Callback: done'] + const metrics = ['findOneAndReplace'] + + helper.runInTransaction(agent, (tx) => { + tx.name = common.TRANSACTION_NAME + collection.findOneAndReplace({ i: 15 }, { b: 15 }, findOpt, function done(error, data) { + assert.equal(error, undefined) + assert.equal(data.ok, 1) + assert.equal(data.value.b, 15) + getValidatorCallback({ t, tx, metrics, segments, end })() + }) + }) + }) + + await t.test('findOneAndUpdate', (t, end) => { + const { agent, collection } = t.nr + const segments = [`${STATEMENT_PREFIX}/findOneAndUpdate`, 'Callback: done'] + const metrics = ['findOneAndUpdate'] + + helper.runInTransaction(agent, (tx) => { + tx.name = common.TRANSACTION_NAME + collection.findOneAndUpdate( + { i: 15 }, + { $set: { a: 15 } }, + findOpt, + function done(error, data) { + assert.equal(error, undefined) + assert.equal(data.ok, 1) + assert.equal(data.value.a, 15) + getValidatorCallback({ t, tx, metrics, segments, end })() + } + ) + }) + }) +}) diff --git a/test/versioned/mongodb-esm/collection-index.tap.mjs b/test/versioned/mongodb-esm/collection-index.tap.mjs deleted file mode 100644 index 4cde25d2c1..0000000000 --- a/test/versioned/mongodb-esm/collection-index.tap.mjs +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import tap from 'tap' -import { test } from './collection-common.mjs' -import helper from '../../lib/agent_helper.js' -import { ESM } from './common.cjs' -const { STATEMENT_PREFIX } = ESM - -tap.test('Collection(Index) Tests', (t) => { - t.autoend() - let agent - - t.before(() => { - agent = helper.instrumentMockedAgent() - }) - - t.teardown(() => { - helper.unloadAgent(agent) - }) - test({ suiteName: 'createIndex', agent, t }, function createIndexTest(t, collection, verify) { - collection.createIndex('i', function onIndex(err, data) { - t.error(err) - t.equal(data, 'i_1') - verify(null, [`${STATEMENT_PREFIX}/createIndex`, 'Callback: onIndex'], ['createIndex']) - }) - }) - - test({ suiteName: 'dropIndex', agent, t }, function dropIndexTest(t, collection, verify) { - collection.createIndex('i', function onIndex(err) { - t.error(err) - collection.dropIndex('i_1', function done(err, data) { - t.error(err) - t.equal(data.ok, 1) - verify( - null, - [ - `${STATEMENT_PREFIX}/createIndex`, - 'Callback: onIndex', - `${STATEMENT_PREFIX}/dropIndex`, - 'Callback: done' - ], - ['createIndex', 'dropIndex'] - ) - }) - }) - }) - - test({ suiteName: 'indexes', agent, t }, function indexesTest(t, collection, verify) { - collection.indexes(function done(err, data) { - t.error(err) - const result = data && data[0] - const expectedResult = { - v: result && result.v, - key: { _id: 1 }, - name: '_id_' - } - - t.same(result, expectedResult, 'should have expected results') - - verify(null, [`${STATEMENT_PREFIX}/indexes`, 'Callback: done'], ['indexes']) - }) - }) - - test({ suiteName: 'indexExists', agent, t }, function indexExistsTest(t, collection, verify) { - collection.indexExists(['_id_'], function done(err, data) { - t.error(err) - t.equal(data, true) - - verify(null, [`${STATEMENT_PREFIX}/indexExists`, 'Callback: done'], ['indexExists']) - }) - }) - - test( - { suiteName: 'indexInformation', agent, t }, - function indexInformationTest(t, collection, verify) { - collection.indexInformation(function done(err, data) { - t.error(err) - t.same(data && data._id_, [['_id', 1]], 'should have expected results') - - verify( - null, - [`${STATEMENT_PREFIX}/indexInformation`, 'Callback: done'], - ['indexInformation'] - ) - }) - } - ) -}) diff --git a/test/versioned/mongodb-esm/collection-index.test.mjs b/test/versioned/mongodb-esm/collection-index.test.mjs new file mode 100644 index 0000000000..6fac529c7a --- /dev/null +++ b/test/versioned/mongodb-esm/collection-index.test.mjs @@ -0,0 +1,108 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import test from 'node:test' +import assert from 'node:assert' +import helper from '../../lib/agent_helper.js' +import { ESM } from './common.cjs' +import { beforeEach, afterEach } from './test-hooks.mjs' +import { getValidatorCallback } from './test-assertions.mjs' +import common from '../mongodb/common.js' + +const { STATEMENT_PREFIX } = ESM + +test('collection index tests', async (t) => { + t.beforeEach(beforeEach) + t.afterEach(afterEach) + + await t.test('createIndex', (t, end) => { + const { agent, collection } = t.nr + const segments = [`${STATEMENT_PREFIX}/createIndex`, 'Callback: onIndex'] + const metrics = ['createIndex'] + + helper.runInTransaction(agent, (tx) => { + tx.name = common.TRANSACTION_NAME + collection.createIndex('i', function onIndex(error, data) { + assert.equal(error, undefined) + assert.equal(data, 'i_1') + getValidatorCallback({ t, tx, metrics, segments, end })() + }) + }) + }) + + await t.test('dropIndex', (t, end) => { + const { agent, collection } = t.nr + const segments = [ + `${STATEMENT_PREFIX}/createIndex`, + 'Callback: onIndex', + `${STATEMENT_PREFIX}/dropIndex`, + 'Callback: done' + ] + const metrics = ['createIndex', 'dropIndex'] + + helper.runInTransaction(agent, (tx) => { + tx.name = common.TRANSACTION_NAME + collection.createIndex('i', function onIndex(error) { + assert.equal(error, undefined) + collection.dropIndex('i_1', function done(erorr, data) { + assert.equal(error, undefined) + assert.equal(data.ok, 1) + getValidatorCallback({ t, tx, metrics, segments, end })() + }) + }) + }) + }) + + await t.test('indexes', (t, end) => { + const { agent, collection } = t.nr + const segments = [`${STATEMENT_PREFIX}/indexes`, 'Callback: done'] + const metrics = ['indexes'] + + helper.runInTransaction(agent, (tx) => { + tx.name = common.TRANSACTION_NAME + collection.indexes('i', function done(error, data) { + assert.equal(error, undefined) + const result = data?.[0] + const expectedResult = { + v: result?.v, + key: { _id: 1 }, + name: '_id_' + } + assert.deepStrictEqual(result, expectedResult, 'should have expected results') + getValidatorCallback({ t, tx, metrics, segments, end })() + }) + }) + }) + + await t.test('indexExists', (t, end) => { + const { agent, collection } = t.nr + const segments = [`${STATEMENT_PREFIX}/indexExists`, 'Callback: done'] + const metrics = ['indexExists'] + + helper.runInTransaction(agent, (tx) => { + tx.name = common.TRANSACTION_NAME + collection.indexExists(['_id_'], function done(error, data) { + assert.equal(error, undefined) + assert.equal(data, true) + getValidatorCallback({ t, tx, metrics, segments, end })() + }) + }) + }) + + await t.test('indexInformation', (t, end) => { + const { agent, collection } = t.nr + const segments = [`${STATEMENT_PREFIX}/indexInformation`, 'Callback: done'] + const metrics = ['indexInformation'] + + helper.runInTransaction(agent, (tx) => { + tx.name = common.TRANSACTION_NAME + collection.indexInformation(function done(error, data) { + assert.equal(error, undefined) + assert.deepStrictEqual(data._id_, [['_id', 1]], 'should have expected results') + getValidatorCallback({ t, tx, metrics, segments, end })() + }) + }) + }) +}) diff --git a/test/versioned/mongodb-esm/collection-misc.tap.mjs b/test/versioned/mongodb-esm/collection-misc.tap.mjs deleted file mode 100644 index 2f7cb78f93..0000000000 --- a/test/versioned/mongodb-esm/collection-misc.tap.mjs +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import tap from 'tap' -import { test, DB_NAME } from './collection-common.mjs' -import helper from '../../lib/agent_helper.js' -import { ESM } from './common.cjs' -const { COLLECTIONS, STATEMENT_PREFIX } = ESM - -function verifyAggregateData(t, data) { - t.equal(data.length, 3, 'should have expected amount of results') - t.same(data, [{ value: 5 }, { value: 15 }, { value: 25 }], 'should have expected results') -} - -tap.test('Collection(Index) Tests', (t) => { - t.autoend() - let agent - - t.before(() => { - agent = helper.instrumentMockedAgent() - }) - - t.teardown(() => { - helper.unloadAgent(agent) - }) - - test( - { suiteName: 'aggregate v4', agent, t }, - async function aggregateTest(t, collection, verify) { - const data = await collection - .aggregate([ - { $sort: { i: 1 } }, - { $match: { mod10: 5 } }, - { $limit: 3 }, - { $project: { value: '$i', _id: 0 } } - ]) - .toArray() - verifyAggregateData(t, data) - verify( - null, - [`${STATEMENT_PREFIX}/aggregate`, `${STATEMENT_PREFIX}/toArray`], - ['aggregate', 'toArray'], - { childrenLength: 2 } - ) - } - ) - - test({ suiteName: 'bulkWrite', agent, t }, function bulkWriteTest(t, collection, verify) { - collection.bulkWrite( - [{ deleteMany: { filter: {} } }, { insertOne: { document: { a: 1 } } }], - { ordered: true, w: 1 }, - onWrite - ) - - function onWrite(err, data) { - t.error(err) - t.equal(data.insertedCount, 1) - t.equal(data.deletedCount, 30) - verify(null, [`${STATEMENT_PREFIX}/bulkWrite`, 'Callback: onWrite'], ['bulkWrite']) - } - }) - - test({ suiteName: 'count', agent, t }, function countTest(t, collection, verify) { - collection.count(function onCount(err, data) { - t.error(err) - t.equal(data, 30) - verify(null, [`${STATEMENT_PREFIX}/count`, 'Callback: onCount'], ['count']) - }) - }) - - test({ suiteName: 'distinct', agent, t }, function distinctTest(t, collection, verify) { - collection.distinct('mod10', function done(err, data) { - t.error(err) - t.same(data.sort(), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) - verify(null, [`${STATEMENT_PREFIX}/distinct`, 'Callback: done'], ['distinct']) - }) - }) - - test({ suiteName: 'drop', agent, t }, function dropTest(t, collection, verify) { - collection.drop(function done(err, data) { - t.error(err) - t.equal(data, true) - verify(null, [`${STATEMENT_PREFIX}/drop`, 'Callback: done'], ['drop']) - }) - }) - - test({ suiteName: 'isCapped', agent, t }, function isCappedTest(t, collection, verify) { - collection.isCapped(function done(err, data) { - t.error(err) - t.notOk(data) - - verify(null, [`${STATEMENT_PREFIX}/isCapped`, 'Callback: done'], ['isCapped']) - }) - }) - - test({ suiteName: 'mapReduce', agent, t }, function mapReduceTest(t, collection, verify) { - collection.mapReduce(map, reduce, { out: { inline: 1 } }, done) - - function done(err, data) { - t.error(err) - const expectedData = [ - { _id: 0, value: 30 }, - { _id: 1, value: 33 }, - { _id: 2, value: 36 }, - { _id: 3, value: 39 }, - { _id: 4, value: 42 }, - { _id: 5, value: 45 }, - { _id: 6, value: 48 }, - { _id: 7, value: 51 }, - { _id: 8, value: 54 }, - { _id: 9, value: 57 } - ] - - // data is not sorted depending on speed of - // db calls, sort to compare vs expected collection - data.sort((a, b) => a._id - b._id) - t.same(data, expectedData) - - verify(null, [`${STATEMENT_PREFIX}/mapReduce`, 'Callback: done'], ['mapReduce']) - } - - /* eslint-disable */ - function map(obj) { - emit(this.mod10, this.i) - } - /* eslint-enable */ - - function reduce(key, vals) { - return vals.reduce(function sum(prev, val) { - return prev + val - }, 0) - } - }) - - test({ suiteName: 'options', agent, t }, function optionsTest(t, collection, verify) { - collection.options(function done(err, data) { - t.error(err) - - // Depending on the version of the mongo server this will change. - if (data) { - t.same(data, {}, 'should have expected results') - } else { - t.notOk(data, 'should have expected results') - } - - verify(null, [`${STATEMENT_PREFIX}/options`, 'Callback: done'], ['options']) - }) - }) - - test({ suiteName: 'rename', agent, t }, function renameTest(t, collection, verify) { - collection.rename(COLLECTIONS.collection2, function done(err) { - t.error(err) - - verify(null, [`${STATEMENT_PREFIX}/rename`, 'Callback: done'], ['rename']) - }) - }) - - test({ suiteName: 'stats', agent, t }, function statsTest(t, collection, verify) { - collection.stats({ i: 5 }, function done(err, data) { - t.error(err) - t.equal(data.ns, `${DB_NAME}.${COLLECTIONS.collection1}`) - t.equal(data.count, 30) - t.equal(data.ok, 1) - - verify(null, [`${STATEMENT_PREFIX}/stats`, 'Callback: done'], ['stats']) - }) - }) -}) diff --git a/test/versioned/mongodb-esm/collection-misc.test.mjs b/test/versioned/mongodb-esm/collection-misc.test.mjs new file mode 100644 index 0000000000..b4d6cef4fb --- /dev/null +++ b/test/versioned/mongodb-esm/collection-misc.test.mjs @@ -0,0 +1,218 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import test from 'node:test' +import assert from 'node:assert' +import helper from '../../lib/agent_helper.js' +import { ESM } from './common.cjs' +import { beforeEach, afterEach } from './test-hooks.mjs' +import { getValidatorCallback } from './test-assertions.mjs' +import common from '../mongodb/common.js' + +const { DB_NAME, COLLECTIONS, STATEMENT_PREFIX } = ESM + +test('collection misc tests', async (t) => { + t.beforeEach(beforeEach) + t.afterEach(afterEach) + + await t.test('aggregate v4', { skip: true }, (t, end) => { + const { agent, collection } = t.nr + const segments = [`${STATEMENT_PREFIX}/aggregate`, `${STATEMENT_PREFIX}/toArray`] + const metrics = ['aggregate', 'toArray'] + + helper.runInTransaction(agent, async (tx) => { + tx.name = common.TRANSACTION_NAME + const data = await collection + .aggregate([ + { $sort: { i: 1 } }, + { $match: { mod10: 5 } }, + { $limit: 3 }, + { $project: { value: '$i', _id: 0 } } + ]) + .toArray() + assert.equal(data.length, 3, 'should have expected amount of results') + assert.deepStrictEqual( + data, + [{ value: 5 }, { value: 15 }, { value: 25 }], + 'should have expected results' + ) + getValidatorCallback({ t, tx, segments, metrics, childrenLength: 2, end })() + }) + }) + + await t.test('bulkWrite', (t, end) => { + const { agent, collection } = t.nr + const segments = [`${STATEMENT_PREFIX}/bulkWrite`, 'Callback: onWrite'] + const metrics = ['bulkWrite'] + + helper.runInTransaction(agent, (tx) => { + tx.name = common.TRANSACTION_NAME + collection.bulkWrite( + [{ deleteMany: { filter: {} } }, { insertOne: { document: { a: 1 } } }], + { ordered: true, w: 1 }, + onWrite + ) + + function onWrite(error, data) { + assert.equal(error, undefined) + assert.equal(data.insertedCount, 1) + assert.equal(data.deletedCount, 30) + getValidatorCallback({ t, tx, segments, metrics, end })() + } + }) + }) + + await t.test('count', (t, end) => { + const { agent, collection } = t.nr + const segments = [`${STATEMENT_PREFIX}/count`, 'Callback: onCount'] + const metrics = ['count'] + + helper.runInTransaction(agent, (tx) => { + tx.name = common.TRANSACTION_NAME + collection.count(function onCount(error, data) { + assert.equal(error, undefined) + assert.equal(data, 30) + getValidatorCallback({ t, tx, segments, metrics, end })() + }) + }) + }) + + await t.test('distinct', (t, end) => { + const { agent, collection } = t.nr + const segments = [`${STATEMENT_PREFIX}/distinct`, 'Callback: done'] + const metrics = ['distinct'] + + helper.runInTransaction(agent, (tx) => { + tx.name = common.TRANSACTION_NAME + collection.distinct('mod10', function done(error, data) { + assert.equal(error, undefined) + assert.deepStrictEqual(data.sort(), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + getValidatorCallback({ t, tx, segments, metrics, end })() + }) + }) + }) + + await t.test('drop', (t, end) => { + const { agent, collection } = t.nr + const segments = [`${STATEMENT_PREFIX}/drop`, 'Callback: done'] + const metrics = ['drop'] + + helper.runInTransaction(agent, (tx) => { + tx.name = common.TRANSACTION_NAME + collection.drop(function done(error, data) { + assert.equal(error, undefined) + assert.equal(data, true) + getValidatorCallback({ t, tx, segments, metrics, end })() + }) + }) + }) + + await t.test('isCapped', (t, end) => { + const { agent, collection } = t.nr + const segments = [`${STATEMENT_PREFIX}/isCapped`, 'Callback: done'] + const metrics = ['isCapped'] + + helper.runInTransaction(agent, (tx) => { + tx.name = common.TRANSACTION_NAME + collection.isCapped(function done(error, data) { + assert.equal(error, undefined) + assert.equal(data, false) + getValidatorCallback({ t, tx, segments, metrics, end })() + }) + }) + }) + + await t.test('mapReduce', (t, end) => { + const { agent, collection } = t.nr + const segments = [`${STATEMENT_PREFIX}/mapReduce`, 'Callback: done'] + const metrics = ['mapReduce'] + + helper.runInTransaction(agent, (tx) => { + tx.name = common.TRANSACTION_NAME + collection.mapReduce(map, reduce, { out: { inline: 1 } }, done) + + function done(error, data) { + assert.equal(error, undefined) + const expectedData = [ + { _id: 0, value: 30 }, + { _id: 1, value: 33 }, + { _id: 2, value: 36 }, + { _id: 3, value: 39 }, + { _id: 4, value: 42 }, + { _id: 5, value: 45 }, + { _id: 6, value: 48 }, + { _id: 7, value: 51 }, + { _id: 8, value: 54 }, + { _id: 9, value: 57 } + ] + + // data is not sorted depending on speed of + // db calls, sort to compare vs expected collection + data.sort((a, b) => a._id - b._id) + assert.deepStrictEqual(data, expectedData) + + getValidatorCallback({ t, tx, segments, metrics, end })() + } + + /* eslint-disable */ + function map(obj) { + emit(this.mod10, this.i) + } + /* eslint-enable */ + + function reduce(key, vals) { + return vals.reduce(function sum(prev, val) { + return prev + val + }, 0) + } + }) + }) + + await t.test('options', (t, end) => { + const { agent, collection } = t.nr + const segments = [`${STATEMENT_PREFIX}/options`, 'Callback: done'] + const metrics = ['options'] + + helper.runInTransaction(agent, (tx) => { + tx.name = common.TRANSACTION_NAME + collection.options(function done(error, data) { + assert.equal(error, undefined) + assert.deepStrictEqual(data, {}, 'should have expected results') + getValidatorCallback({ t, tx, segments, metrics, end })() + }) + }) + }) + + await t.test('rename', (t, end) => { + const { agent, collection } = t.nr + const segments = [`${STATEMENT_PREFIX}/rename`, 'Callback: done'] + const metrics = ['rename'] + + helper.runInTransaction(agent, (tx) => { + tx.name = common.TRANSACTION_NAME + collection.rename(COLLECTIONS.collection2, function done(error) { + assert.equal(error, undefined) + getValidatorCallback({ t, tx, segments, metrics, end })() + }) + }) + }) + + await t.test('stats', (t, end) => { + const { agent, collection } = t.nr + const segments = [`${STATEMENT_PREFIX}/stats`, 'Callback: done'] + const metrics = ['stats'] + + helper.runInTransaction(agent, (tx) => { + tx.name = common.TRANSACTION_NAME + collection.stats({ i: 5 }, function done(error, data) { + assert.equal(error, undefined) + assert.equal(data.ns, `${DB_NAME}.${COLLECTIONS.collection1}`) + assert.equal(data.count, 30) + assert.equal(data.ok, 1) + getValidatorCallback({ t, tx, segments, metrics, end })() + }) + }) + }) +}) diff --git a/test/versioned/mongodb-esm/collection-update.tap.mjs b/test/versioned/mongodb-esm/collection-update.tap.mjs deleted file mode 100644 index 5aab4f724b..0000000000 --- a/test/versioned/mongodb-esm/collection-update.tap.mjs +++ /dev/null @@ -1,212 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import tap from 'tap' -import { test } from './collection-common.mjs' -import helper from '../../lib/agent_helper.js' -import { ESM } from './common.cjs' -const { STATEMENT_PREFIX } = ESM - -/** - * The response from the methods in this file differ between versions - * This helper decides which pieces to assert - * - * @param {Object} params - * @param {Tap.Test} params.t - * @param {Object} params.data result from callback used to assert - * @param {Number} params.count, optional - * @param {string} params.keyPrefix prefix where the count exists - * @param {Object} params.extraValues extra fields to assert on >=4.0.0 version of module - */ -function assertExpectedResult({ t, data, count, keyPrefix, extraValues }) { - const expectedResult = { acknowledged: true, ...extraValues } - if (count) { - expectedResult[`${keyPrefix}Count`] = count - } - t.same(data, expectedResult) -} - -tap.test('Collection(Update) Tests', (t) => { - t.autoend() - let agent - - t.before(() => { - agent = helper.instrumentMockedAgent() - }) - - t.teardown(() => { - helper.unloadAgent(agent) - }) - - test({ suiteName: 'deleteMany', agent, t }, function deleteManyTest(t, collection, verify) { - collection.deleteMany({ mod10: 5 }, function done(err, data) { - t.error(err) - assertExpectedResult({ - t, - data, - count: 3, - keyPrefix: 'deleted' - }) - verify(null, [`${STATEMENT_PREFIX}/deleteMany`, 'Callback: done'], ['deleteMany']) - }) - }) - - test({ suiteName: 'deleteOne', agent, t }, function deleteOneTest(t, collection, verify) { - collection.deleteOne({ mod10: 5 }, function done(err, data) { - t.error(err) - assertExpectedResult({ - t, - data, - count: 1, - keyPrefix: 'deleted' - }) - verify(null, [`${STATEMENT_PREFIX}/deleteOne`, 'Callback: done'], ['deleteOne']) - }) - }) - - test({ suiteName: 'insert', agent, t }, function insertTest(t, collection, verify) { - collection.insert({ foo: 'bar' }, function done(err, data) { - t.error(err) - assertExpectedResult({ - t, - data, - count: 1, - keyPrefix: 'inserted', - extraValues: { - insertedIds: { - 0: {} - } - } - }) - - verify(null, [`${STATEMENT_PREFIX}/insert`, 'Callback: done'], ['insert']) - }) - }) - - test({ suiteName: 'insertMany', agent, t }, function insertManyTest(t, collection, verify) { - collection.insertMany([{ foo: 'bar' }, { foo: 'bar2' }], function done(err, data) { - t.error(err) - assertExpectedResult({ - t, - data, - count: 2, - keyPrefix: 'inserted', - extraValues: { - insertedIds: { - 0: {}, - 1: {} - } - } - }) - - verify(null, [`${STATEMENT_PREFIX}/insertMany`, 'Callback: done'], ['insertMany']) - }) - }) - - test({ suiteName: 'insertOne', agent, t }, function insertOneTest(t, collection, verify) { - collection.insertOne({ foo: 'bar' }, function done(err, data) { - t.error(err) - assertExpectedResult({ - t, - data, - extraValues: { - insertedId: {} - } - }) - - verify(null, [`${STATEMENT_PREFIX}/insertOne`, 'Callback: done'], ['insertOne']) - }) - }) - - test({ suiteName: 'remove', agent, t }, function removeTest(t, collection, verify) { - collection.remove({ mod10: 5 }, function done(err, data) { - t.error(err) - assertExpectedResult({ - t, - data, - count: 3, - keyPrefix: 'deleted' - }) - - verify(null, [`${STATEMENT_PREFIX}/remove`, 'Callback: done'], ['remove']) - }) - }) - - test({ suiteName: 'replaceOne', agent, t }, function replaceOneTest(t, collection, verify) { - collection.replaceOne({ i: 5 }, { foo: 'bar' }, function done(err, data) { - t.error(err) - assertExpectedResult({ - t, - data, - count: 1, - keyPrefix: 'modified', - extraValues: { - matchedCount: 1, - upsertedCount: 0, - upsertedId: null - } - }) - - verify(null, [`${STATEMENT_PREFIX}/replaceOne`, 'Callback: done'], ['replaceOne']) - }) - }) - - test({ suiteName: 'update', agent, t }, function updateTest(t, collection, verify) { - collection.update({ i: 5 }, { $set: { foo: 'bar' } }, function done(err, data) { - t.error(err) - assertExpectedResult({ - t, - data, - count: 1, - keyPrefix: 'modified', - extraValues: { - matchedCount: 1, - upsertedCount: 0, - upsertedId: null - } - }) - - verify(null, [`${STATEMENT_PREFIX}/update`, 'Callback: done'], ['update']) - }) - }) - - test({ suiteName: 'updateMany', agent, t }, function updateManyTest(t, collection, verify) { - collection.updateMany({ mod10: 5 }, { $set: { a: 5 } }, function done(err, data) { - t.error(err) - assertExpectedResult({ - t, - data, - count: 3, - keyPrefix: 'modified', - extraValues: { - matchedCount: 3, - upsertedCount: 0, - upsertedId: null - } - }) - - verify(null, [`${STATEMENT_PREFIX}/updateMany`, 'Callback: done'], ['updateMany']) - }) - }) - - test({ suiteName: 'updateOne', agent, t }, function updateOneTest(t, collection, verify) { - collection.updateOne({ i: 5 }, { $set: { a: 5 } }, function done(err, data) { - t.notOk(err, 'should not error') - assertExpectedResult({ - t, - data, - count: 1, - keyPrefix: 'modified', - extraValues: { - matchedCount: 1, - upsertedCount: 0, - upsertedId: null - } - }) - - verify(null, [`${STATEMENT_PREFIX}/updateOne`, 'Callback: done'], ['updateOne']) - }) - }) -}) diff --git a/test/versioned/mongodb-esm/collection-update.test.mjs b/test/versioned/mongodb-esm/collection-update.test.mjs new file mode 100644 index 0000000000..f2fb6d7e27 --- /dev/null +++ b/test/versioned/mongodb-esm/collection-update.test.mjs @@ -0,0 +1,225 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import test from 'node:test' +import assert from 'node:assert' +import helper from '../../lib/agent_helper.js' +import { ESM } from './common.cjs' +import { beforeEach, afterEach } from './test-hooks.mjs' +import { getValidatorCallback, matchObject } from './test-assertions.mjs' +import common from '../mongodb/common.js' + +const { STATEMENT_PREFIX } = ESM + +/** + * The response from the methods in this file differ between versions + * This helper decides which pieces to assert + * + * @param {Object} params + * @param {Object} params.data result from callback used to assert + * @param {Number} params.count, optional + * @param {string} params.keyPrefix prefix where the count exists + * @param {Object} params.extraValues extra fields to assert on >=4.0.0 version of module + */ +function assertExpectedResult({ data, count, keyPrefix, extraValues }) { + const expectedResult = { acknowledged: true, ...extraValues } + if (count) { + expectedResult[`${keyPrefix}Count`] = count + } + matchObject(data, expectedResult) +} + +test('collection update tests', async (t) => { + t.beforeEach(beforeEach) + t.afterEach(afterEach) + + await t.test('deleteMany', (t, end) => { + const { agent, collection } = t.nr + const segments = [`${STATEMENT_PREFIX}/deleteMany`, 'Callback: done'] + const metrics = ['deleteMany'] + + helper.runInTransaction(agent, (tx) => { + tx.name = common.TRANSACTION_NAME + collection.deleteMany({ mod10: 5 }, function done(error, data) { + assert.equal(error, undefined) + assertExpectedResult({ data, count: 3, keyPrefix: 'deleted' }) + getValidatorCallback({ t, tx, segments, metrics, end })() + }) + }) + }) + + await t.test('deleteOne', (t, end) => { + const { agent, collection } = t.nr + const segments = [`${STATEMENT_PREFIX}/deleteOne`, 'Callback: done'] + const metrics = ['deleteOne'] + + helper.runInTransaction(agent, (tx) => { + tx.name = common.TRANSACTION_NAME + collection.deleteOne({ mod10: 5 }, function done(error, data) { + assert.equal(error, undefined) + assertExpectedResult({ data, count: 1, keyPrefix: 'deleted' }) + getValidatorCallback({ t, tx, segments, metrics, end })() + }) + }) + }) + + await t.test('insert', (t, end) => { + const { agent, collection } = t.nr + const segments = [`${STATEMENT_PREFIX}/insert`, 'Callback: done'] + const metrics = ['insert'] + + helper.runInTransaction(agent, (tx) => { + tx.name = common.TRANSACTION_NAME + collection.insert({ foo: 'bar' }, function done(error, data) { + assert.equal(error, undefined) + assertExpectedResult({ + data, + count: 1, + keyPrefix: 'inserted', + extraValues: { insertedIds: { 0: {} } } + }) + getValidatorCallback({ t, tx, segments, metrics, end })() + }) + }) + }) + + await t.test('insertMany', (t, end) => { + const { agent, collection } = t.nr + const segments = [`${STATEMENT_PREFIX}/insertMany`, 'Callback: done'] + const metrics = ['insertMany'] + + helper.runInTransaction(agent, (tx) => { + tx.name = common.TRANSACTION_NAME + collection.insertMany([{ foo: 'bar' }, { foo: 'bar2' }], function done(error, data) { + assert.equal(error, undefined) + assertExpectedResult({ + data, + count: 2, + keyPrefix: 'inserted', + extraValues: { insertedIds: { 0: {}, 1: {} } } + }) + getValidatorCallback({ t, tx, segments, metrics, end })() + }) + }) + }) + + await t.test('insertOne', (t, end) => { + const { agent, collection } = t.nr + const segments = [`${STATEMENT_PREFIX}/insertOne`, 'Callback: done'] + const metrics = ['insertOne'] + + helper.runInTransaction(agent, (tx) => { + tx.name = common.TRANSACTION_NAME + collection.insertOne({ foo: 'bar' }, function done(error, data) { + assert.equal(error, undefined) + assertExpectedResult({ + data, + keyPrefix: 'inserted', + extraValues: { insertedId: {} } + }) + getValidatorCallback({ t, tx, segments, metrics, end })() + }) + }) + }) + + await t.test('remove', (t, end) => { + const { agent, collection } = t.nr + const segments = [`${STATEMENT_PREFIX}/remove`, 'Callback: done'] + const metrics = ['remove'] + + helper.runInTransaction(agent, (tx) => { + tx.name = common.TRANSACTION_NAME + collection.remove({ mod10: 5 }, function done(error, data) { + assert.equal(error, undefined) + assertExpectedResult({ + data, + count: 3, + keyPrefix: 'deleted' + }) + getValidatorCallback({ t, tx, segments, metrics, end })() + }) + }) + }) + + await t.test('replaceOne', (t, end) => { + const { agent, collection } = t.nr + const segments = [`${STATEMENT_PREFIX}/replaceOne`, 'Callback: done'] + const metrics = ['replaceOne'] + + helper.runInTransaction(agent, (tx) => { + tx.name = common.TRANSACTION_NAME + collection.replaceOne({ i: 5 }, { foo: 'bar' }, function done(error, data) { + assert.equal(error, undefined) + assertExpectedResult({ + data, + count: 1, + keyPrefix: 'modified', + extraValues: { matchedCount: 1, upsertedCount: 0, upsertedId: null } + }) + getValidatorCallback({ t, tx, segments, metrics, end })() + }) + }) + }) + + await t.test('update', (t, end) => { + const { agent, collection } = t.nr + const segments = [`${STATEMENT_PREFIX}/update`, 'Callback: done'] + const metrics = ['update'] + + helper.runInTransaction(agent, (tx) => { + tx.name = common.TRANSACTION_NAME + collection.update({ i: 5 }, { $set: { foo: 'bar' } }, function done(error, data) { + assert.equal(error, undefined) + assertExpectedResult({ + data, + count: 1, + keyPrefix: 'modified', + extraValues: { matchedCount: 1, upsertedCount: 0, upsertedId: null } + }) + getValidatorCallback({ t, tx, segments, metrics, end })() + }) + }) + }) + + await t.test('updateMany', (t, end) => { + const { agent, collection } = t.nr + const segments = [`${STATEMENT_PREFIX}/updateMany`, 'Callback: done'] + const metrics = ['updateMany'] + + helper.runInTransaction(agent, (tx) => { + tx.name = common.TRANSACTION_NAME + collection.updateMany({ mod10: 5 }, { $set: { a: 5 } }, function done(error, data) { + assert.equal(error, undefined) + assertExpectedResult({ + data, + count: 3, + keyPrefix: 'modified', + extraValues: { matchedCount: 3, upsertedCount: 0, upsertedId: null } + }) + getValidatorCallback({ t, tx, segments, metrics, end })() + }) + }) + }) + + await t.test('updateOne', (t, end) => { + const { agent, collection } = t.nr + const segments = [`${STATEMENT_PREFIX}/updateOne`, 'Callback: done'] + const metrics = ['updateOne'] + + helper.runInTransaction(agent, (tx) => { + tx.name = common.TRANSACTION_NAME + collection.updateOne({ i: 5 }, { $set: { a: 5 } }, function done(error, data) { + assert.equal(error, undefined) + assertExpectedResult({ + data, + count: 1, + keyPrefix: 'modified', + extraValues: { matchedCount: 1, upsertedCount: 0, upsertedId: null } + }) + getValidatorCallback({ t, tx, segments, metrics, end })() + }) + }) + }) +}) diff --git a/test/versioned/mongodb-esm/cursor.tap.mjs b/test/versioned/mongodb-esm/cursor.tap.mjs deleted file mode 100644 index fb9f7a4c00..0000000000 --- a/test/versioned/mongodb-esm/cursor.tap.mjs +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import tap from 'tap' -import helper from '../../lib/agent_helper.js' -import { test } from './collection-common.mjs' -import { ESM } from './common.cjs' -const { STATEMENT_PREFIX } = ESM - -tap.test('Cursor Tests', (t) => { - t.autoend() - let agent - - t.before(() => { - agent = helper.instrumentMockedAgent() - }) - - t.teardown(() => { - helper.unloadAgent(agent) - }) - - test({ suiteName: 'count', agent, t }, function countTest(t, collection, verify) { - collection.find({}).count(function onCount(err, data) { - t.notOk(err, 'should not error') - t.equal(data, 30, 'should have correct result') - verify(null, [`${STATEMENT_PREFIX}/count`, 'Callback: onCount'], ['count']) - }) - }) - - test({ suiteName: 'explain', agent, t }, function explainTest(t, collection, verify) { - collection.find({}).explain(function onExplain(err, data) { - t.error(err) - // Depending on the version of the mongo server the explain plan is different. - if (data.hasOwnProperty('cursor')) { - t.equal(data.cursor, 'BasicCursor', 'should have correct response') - } else { - t.ok(data.hasOwnProperty('queryPlanner'), 'should have correct response') - } - verify(null, [`${STATEMENT_PREFIX}/explain`, 'Callback: onExplain'], ['explain']) - }) - }) - - test({ suiteName: 'next', agent, t }, function nextTest(t, collection, verify) { - collection.find({}).next(function onNext(err, data) { - t.notOk(err) - t.equal(data.i, 0) - verify(null, [`${STATEMENT_PREFIX}/next`, 'Callback: onNext'], ['next']) - }) - }) - - test({ suiteName: 'toArray', agent, t }, function toArrayTest(t, collection, verify) { - collection.find({}).toArray(function onToArray(err, data) { - t.notOk(err) - t.equal(data[0].i, 0) - verify(null, [`${STATEMENT_PREFIX}/toArray`, 'Callback: onToArray'], ['toArray']) - }) - }) -}) diff --git a/test/versioned/mongodb-esm/cursor.test.mjs b/test/versioned/mongodb-esm/cursor.test.mjs new file mode 100644 index 0000000000..fda59888eb --- /dev/null +++ b/test/versioned/mongodb-esm/cursor.test.mjs @@ -0,0 +1,83 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import test from 'node:test' +import assert from 'node:assert' +import helper from '../../lib/agent_helper.js' +import { ESM } from './common.cjs' +import { beforeEach, afterEach } from './test-hooks.mjs' +import { getValidatorCallback } from './test-assertions.mjs' +import common from '../mongodb/common.js' + +const { DB_NAME, COLLECTIONS, STATEMENT_PREFIX } = ESM + +test('cursor tests', async (t) => { + t.beforeEach(beforeEach) + t.afterEach(afterEach) + + await t.test('count', (t, end) => { + const { agent, collection } = t.nr + const segments = [`${STATEMENT_PREFIX}/count`, 'Callback: onCount'] + const metrics = ['count'] + + helper.runInTransaction(agent, (tx) => { + tx.name = common.TRANSACTION_NAME + collection.find({}).count(function onCount(error, data) { + assert.equal(error, undefined, 'should not error') + assert.equal(data, 30, 'should have correct result') + getValidatorCallback({ t, tx, segments, metrics, end })() + }) + }) + }) + + await t.test('explain', (t, end) => { + const { agent, collection } = t.nr + const segments = [`${STATEMENT_PREFIX}/explain`, 'Callback: onExplain'] + const metrics = ['explain'] + + helper.runInTransaction(agent, (tx) => { + tx.name = common.TRANSACTION_NAME + collection.find({}).explain(function onExplain(error, data) { + assert.equal(error, undefined, 'should not error') + assert.equal( + data.queryPlanner.namespace, + `${DB_NAME}.${COLLECTIONS.collection1}`, + 'should have correct result' + ) + getValidatorCallback({ t, tx, segments, metrics, end })() + }) + }) + }) + + await t.test('next', (t, end) => { + const { agent, collection } = t.nr + const segments = [`${STATEMENT_PREFIX}/next`, 'Callback: onNext'] + const metrics = ['next'] + + helper.runInTransaction(agent, (tx) => { + tx.name = common.TRANSACTION_NAME + collection.find({}).next(function onNext(error, data) { + assert.equal(error, undefined, 'should not error') + assert.equal(data.i, 0, 'should have correct result') + getValidatorCallback({ t, tx, segments, metrics, end })() + }) + }) + }) + + await t.test('toArray', (t, end) => { + const { agent, collection } = t.nr + const segments = [`${STATEMENT_PREFIX}/toArray`, 'Callback: onToArray'] + const metrics = ['toArray'] + + helper.runInTransaction(agent, (tx) => { + tx.name = common.TRANSACTION_NAME + collection.find({}).toArray(function onToArray(error, data) { + assert.equal(error, undefined, 'should not error') + assert.equal(data[0].i, 0, 'should have correct result') + getValidatorCallback({ t, tx, segments, metrics, end })() + }) + }) + }) +}) diff --git a/test/versioned/mongodb-esm/db.tap.mjs b/test/versioned/mongodb-esm/db.tap.mjs deleted file mode 100644 index b193a2361d..0000000000 --- a/test/versioned/mongodb-esm/db.tap.mjs +++ /dev/null @@ -1,323 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import tap from 'tap' -import { DB_NAME, dropTestCollections } from './collection-common.mjs' -import helper from '../../lib/agent_helper.js' -import { getHostName, getPort, connect, close, ESM } from './common.cjs' -const { COLLECTIONS } = ESM - -let MONGO_HOST = null -let MONGO_PORT = null -const BAD_MONGO_COMMANDS = ['collection'] - -tap.test('Db tests', (t) => { - t.autoend() - let agent - let mongodb - - t.before(async () => { - agent = helper.instrumentMockedAgent() - const mongoPkg = await import('mongodb') - mongodb = mongoPkg.default - }) - - t.teardown(() => { - helper.unloadAgent(agent) - }) - - t.beforeEach(() => { - return dropTestCollections(mongodb) - }) - - t.test('addUser, authenticate, removeUser', (t) => { - dbTest({ t, agent, mongodb }, function addUserTest(t, db, verify) { - const userName = 'user-test' - const userPass = 'user-test-pass' - - db.removeUser(userName, function preRemove() { - // Don't care if this first remove fails, it's just to ensure a clean slate. - db.addUser(userName, userPass, { roles: ['readWrite'] }, added) - }) - - function added(err) { - if (!t.error(err, 'addUser should not have error')) { - return t.end() - } - - if (typeof db.authenticate === 'function') { - db.authenticate(userName, userPass, authed) - } else { - t.comment('Skipping authentication test, not supported on db') - db.removeUser(userName, removedNoAuth) - } - } - - function authed(err) { - if (!t.error(err, 'authenticate should not have error')) { - return t.end() - } - db.removeUser(userName, removed) - } - - function removed(err) { - if (!t.error(err, 'removeUser should not have error')) { - return t.end() - } - verify([ - 'Datastore/operation/MongoDB/removeUser', - 'Callback: preRemove', - 'Datastore/operation/MongoDB/addUser', - 'Callback: added', - 'Datastore/operation/MongoDB/authenticate', - 'Callback: authed', - 'Datastore/operation/MongoDB/removeUser', - 'Callback: removed' - ]) - } - - function removedNoAuth(err) { - if (!t.error(err, 'removeUser should not have error')) { - return t.end() - } - verify([ - 'Datastore/operation/MongoDB/removeUser', - 'Callback: preRemove', - 'Datastore/operation/MongoDB/addUser', - 'Callback: added', - 'Datastore/operation/MongoDB/removeUser', - 'Callback: removedNoAuth' - ]) - } - }) - }) - - t.test('collections', (t) => { - dbTest({ t, agent, mongodb }, function collectionTest(t, db, verify) { - db.collections(function gotCollections(err2, collections) { - t.error(err2, 'should not have error') - t.ok(Array.isArray(collections), 'got array of collections') - verify(['Datastore/operation/MongoDB/collections', 'Callback: gotCollections']) - }) - }) - }) - - t.test('command', (t) => { - dbTest({ t, agent, mongodb }, function collectionTest(t, db, verify) { - db.command({ ping: 1 }, function onCommand(err, result) { - t.error(err, 'should not have error') - t.same(result, { ok: 1 }, 'got correct result') - verify(['Datastore/operation/MongoDB/command', 'Callback: onCommand']) - }) - }) - }) - - t.test('createCollection', (t) => { - dbTest({ t, agent, mongodb, dropCollections: true }, function collectionTest(t, db, verify) { - db.createCollection(COLLECTIONS.collection1, function gotCollection(err, collection) { - t.error(err, 'should not have error') - t.equal( - collection.collectionName || collection.s.name, - COLLECTIONS.collection1, - 'new collection should have the right name' - ) - verify(['Datastore/operation/MongoDB/createCollection', 'Callback: gotCollection']) - }) - }) - }) - - t.test('createIndex', (t) => { - dbTest({ t, agent, mongodb }, function collectionTest(t, db, verify) { - db.createIndex(COLLECTIONS.collection1, 'foo', function createdIndex(err, result) { - t.error(err, 'should not have error') - t.equal(result, 'foo_1', 'should have the right result') - verify(['Datastore/operation/MongoDB/createIndex', 'Callback: createdIndex']) - }) - }) - }) - - t.test('dropCollection', (t) => { - dbTest({ t, agent, mongodb }, function collectionTest(t, db, verify) { - db.createCollection(COLLECTIONS.collection1, function gotCollection(err) { - t.error(err, 'should not have error getting collection') - - db.dropCollection(COLLECTIONS.collection1, function droppedCollection(err, result) { - t.error(err, 'should not have error dropping collection') - t.ok(result === true, 'result should be boolean true') - verify([ - 'Datastore/operation/MongoDB/createCollection', - 'Callback: gotCollection', - 'Datastore/operation/MongoDB/dropCollection', - 'Callback: droppedCollection' - ]) - }) - }) - }) - }) - - t.test('dropDatabase', (t) => { - dbTest({ t, agent, mongodb }, function collectionTest(t, db, verify) { - db.dropDatabase(function droppedDatabase(err, result) { - t.error(err, 'should not have error') - t.ok(result, 'result should be truthy') - verify(['Datastore/operation/MongoDB/dropDatabase', 'Callback: droppedDatabase']) - }) - }) - }) - - t.test('indexInformation', (t) => { - dbTest({ t, agent, mongodb }, function collectionTest(t, db, verify) { - db.createIndex(COLLECTIONS.collection1, 'foo', function createdIndex(err) { - t.error(err, 'createIndex should not have error') - db.indexInformation(COLLECTIONS.collection1, function gotInfo(err2, result) { - t.error(err2, 'indexInformation should not have error') - t.same( - result, - { _id_: [['_id', 1]], foo_1: [['foo', 1]] }, - 'result is the expected object' - ) - verify([ - 'Datastore/operation/MongoDB/createIndex', - 'Callback: createdIndex', - 'Datastore/operation/MongoDB/indexInformation', - 'Callback: gotInfo' - ]) - }) - }) - }) - }) - - t.test('renameCollection', (t) => { - dbTest({ t, agent, mongodb }, function collectionTest(t, db, verify) { - db.createCollection(COLLECTIONS.collection1, function gotCollection(err) { - t.error(err, 'should not have error getting collection') - db.renameCollection( - COLLECTIONS.collection1, - COLLECTIONS.collection2, - function renamedCollection(err2) { - t.error(err2, 'should not have error renaming collection') - db.dropCollection(COLLECTIONS.collection2, function droppedCollection(err3) { - t.error(err3) - verify([ - 'Datastore/operation/MongoDB/createCollection', - 'Callback: gotCollection', - 'Datastore/operation/MongoDB/renameCollection', - 'Callback: renamedCollection', - 'Datastore/operation/MongoDB/dropCollection', - 'Callback: droppedCollection' - ]) - }) - } - ) - }) - }) - }) - - t.test('stats', (t) => { - dbTest({ t, agent, mongodb }, function collectionTest(t, db, verify) { - db.stats({}, function gotStats(err, stats) { - t.error(err, 'should not have error') - t.ok(stats, 'got stats') - verify(['Datastore/operation/MongoDB/stats', 'Callback: gotStats']) - }) - }) - }) -}) - -function dbTest({ t, agent, mongodb }, run) { - let db = null - let client = null - - t.autoend() - - t.beforeEach(async function () { - MONGO_HOST = getHostName(agent) - MONGO_PORT = getPort() - - const res = await connect({ mongodb, name: DB_NAME }) - client = res.client - db = res.db - }) - - t.afterEach(function () { - return close(client, db) - }) - - t.test('without transaction', function (t) { - run(t, db, function () { - t.notOk(agent.getTransaction(), 'should not have transaction') - t.end() - }) - }) - - t.test('with transaction', function (t) { - t.notOk(agent.getTransaction(), 'should not have transaction') - helper.runInTransaction(agent, function (transaction) { - run(t, db, function (names) { - verifyMongoSegments(t, agent, transaction, names) - transaction.end() - t.end() - }) - }) - }) -} - -function verifyMongoSegments(t, agent, transaction, names) { - t.ok(agent.getTransaction(), 'should not lose transaction state') - t.equal(agent.getTransaction().id, transaction.id, 'transaction is correct') - - const segment = agent.tracer.getSegment() - let current = transaction.trace.root - - for (let i = 0, l = names.length; i < l; ++i) { - // Filter out net.createConnection segments as they could occur during execution, which is fine - // but breaks out assertion function - current.children = current.children.filter((child) => child.name !== 'net.createConnection') - t.equal(current.children.length, 1, 'should have one child segment') - current = current.children[0] - t.equal(current.name, names[i], 'segment should be named ' + names[i]) - - // If this is a Mongo operation/statement segment then it should have the - // datastore instance attributes. - if (/^Datastore\/.*?\/MongoDB/.test(current.name)) { - if (isBadSegment(current)) { - t.comment('Skipping attributes check for ' + current.name) - continue - } - - // Commands known as "admin commands" always happen against the "admin" - // database regardless of the DB the connection is actually connected to. - // This is apparently by design. - // https://jira.mongodb.org/browse/NODE-827 - let dbName = DB_NAME - if (/\/renameCollection$/.test(current.name)) { - dbName = 'admin' - } - - const attributes = current.getAttributes() - t.equal(attributes.database_name, dbName, 'should have correct db name') - t.equal(attributes.host, MONGO_HOST, 'should have correct host name') - t.equal(attributes.port_path_or_id, MONGO_PORT, 'should have correct port') - t.equal(attributes.product, 'MongoDB', 'should have correct product attribute') - } - } - - // Do not use `t.equal` for this comparison. When it is false tap would dump - // way too much information to be useful. - t.ok(current === segment, 'current segment is ' + segment.name) -} - -function isBadSegment(segment) { - const nameParts = segment.name.split('/') - const command = nameParts[nameParts.length - 1] - const attributes = segment.getAttributes() - - return ( - BAD_MONGO_COMMANDS.indexOf(command) !== -1 && // Is in the list of bad commands - !attributes.database_name && // and does not have any of the - !attributes.host && // instance attributes. - !attributes.port_path_or_id - ) -} diff --git a/test/versioned/mongodb-esm/db.test.mjs b/test/versioned/mongodb-esm/db.test.mjs new file mode 100644 index 0000000000..c9da524aea --- /dev/null +++ b/test/versioned/mongodb-esm/db.test.mjs @@ -0,0 +1,513 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import test from 'node:test' +import assert from 'node:assert' +import helper from '../../lib/agent_helper.js' +import { ESM } from './common.cjs' +import { beforeEach, afterEach, dropTestCollections } from './test-hooks.mjs' +import { matchObject } from './test-assertions.mjs' + +const { DB_NAME, COLLECTIONS } = ESM +const BAD_MONGO_COMMANDS = ['collection'] + +test('addUser, authenticate, removeUser', async (t) => { + t.beforeEach(beforeEach) + t.afterEach(afterEach) + + await t.test('without transaction', (t, end) => { + const { agent, db } = t.nr + doWork(db, () => { + assert.equal(agent.getTransaction(), undefined, 'should not have transaction') + end() + }) + }) + + await t.test('with transaction', (t, end) => { + const { agent, db } = t.nr + assert.equal(agent.getTransaction(), undefined, 'should not have transaction') + helper.runInTransaction(agent, (tx) => { + doWork(db, (expectedSegments) => { + verifyMongoSegments({ t, tx, expectedSegments }) + tx.end() + end() + }) + }) + }) + + function doWork(db, done) { + const username = 'user-test' + const password = 'user-test-pass' + + db.removeUser(username, function preRemove() { + // Don't care if this first remove fails. It's just to ensure a clean slate. + db.addUser(username, password, { roles: ['readWrite'] }, added) + }) + + function added(error) { + assert.equal(error, undefined, 'addUser should not have error') + if (typeof db.authenticate === 'function') { + db.authenticate(username, password, authed) + } else { + t.diagnostic('skipping authentication test, not supported on db') + db.removeUser(username, removedNoAuth) + } + } + + function authed(error) { + assert.equal(error, undefined, 'authenticate should not have errored') + db.removeUser(username, removed) + } + + function removed(error) { + assert.equal(error, undefined, 'removeUser should not have errored') + const expectedSegments = [ + 'Datastore/operation/MongoDB/removeUser', + 'Callback: preRemove', + 'Datastore/operation/MongoDB/addUser', + 'Callback: added', + 'Datastore/operation/MongoDB/authenticate', + 'Callback: authed', + 'Datastore/operation/MongoDB/removeUser', + 'Callback: removed' + ] + done(expectedSegments) + } + + function removedNoAuth(error) { + assert.equal(error, undefined, 'removeUser should not have errored') + const expectedSegments = [ + 'Datastore/operation/MongoDB/removeUser', + 'Callback: preRemove', + 'Datastore/operation/MongoDB/addUser', + 'Callback: added', + 'Datastore/operation/MongoDB/removeUser', + 'Callback: removedNoAuth' + ] + done(expectedSegments) + } + } +}) + +test('collections', async (t) => { + t.beforeEach(beforeEach) + t.afterEach(afterEach) + + await t.test('without transaction', (t, end) => { + const { agent, db } = t.nr + db.collections((error, collections) => { + assert.equal(error, undefined) + assert.equal(Array.isArray(collections), true, 'got array of collections') + assert.equal(agent.getTransaction(), undefined, 'should not have transaction') + end() + }) + }) + + await t.test('with transaction', (t, end) => { + const { agent, db } = t.nr + helper.runInTransaction(agent, (tx) => { + db.collections(function gotCollections(error, collections) { + assert.equal(error, undefined) + assert.equal(Array.isArray(collections), true, 'got array of collections') + verifyMongoSegments({ + t, + tx, + expectedSegments: ['Datastore/operation/MongoDB/collections', 'Callback: gotCollections'] + }) + tx.end() + end() + }) + }) + }) +}) + +test('command', async (t) => { + t.beforeEach(beforeEach) + t.afterEach(afterEach) + + await t.test('without transaction', (t, end) => { + const { agent, db } = t.nr + db.command({ ping: 1 }, (error, result) => { + assert.equal(error, undefined) + assert.deepStrictEqual(result, { ok: 1 }, 'got correct result') + assert.equal(agent.getTransaction(), undefined, 'should not have transaction') + end() + }) + }) + + await t.test('with transaction', (t, end) => { + const { agent, db } = t.nr + helper.runInTransaction(agent, (tx) => { + db.command({ ping: 1 }, function onCommand(error, result) { + assert.equal(error, undefined) + assert.deepStrictEqual(result, { ok: 1 }, 'got correct result') + verifyMongoSegments({ + t, + tx, + expectedSegments: ['Datastore/operation/MongoDB/command', 'Callback: onCommand'] + }) + tx.end() + end() + }) + }) + }) +}) + +test('createCollection', async (t) => { + t.beforeEach(beforeEach) + t.afterEach(afterEach) + + await t.test('without transaction', (t, end) => { + const { agent, db, mongodb } = t.nr + dropTestCollections(mongodb).then(() => { + db.createCollection(COLLECTIONS.collection1, (error, collection) => { + assert.equal(error, undefined) + assert.equal( + collection.collectionName || collection.s.name, + COLLECTIONS.collection1, + 'new collection should have the right name' + ) + assert.equal(agent.getTransaction(), undefined, 'should not have transaction') + end() + }) + }) + }) + + await t.test('with transaction', (t, end) => { + const { agent, db, mongodb } = t.nr + dropTestCollections(mongodb).then(() => { + helper.runInTransaction(agent, (tx) => { + db.createCollection(COLLECTIONS.collection1, function gotCollection(error, collection) { + assert.equal(error, undefined) + assert.equal( + collection.collectionName || collection.s.name, + COLLECTIONS.collection1, + 'new collection should have the right name' + ) + verifyMongoSegments({ + t, + tx, + expectedSegments: [ + 'Datastore/operation/MongoDB/createCollection', + 'Callback: gotCollection' + ] + }) + tx.end() + end() + }) + }) + }) + }) +}) + +test('createIndex', async (t) => { + t.beforeEach(beforeEach) + t.afterEach(afterEach) + + await t.test('without transaction', (t, end) => { + const { agent, db } = t.nr + db.createIndex(COLLECTIONS.collection1, 'foo', (error, result) => { + assert.equal(error, undefined) + assert.equal(result, 'foo_1', 'should have right result') + assert.equal(agent.getTransaction(), undefined, 'should not have transaction') + end() + }) + }) + + await t.test('with transaction', (t, end) => { + const { agent, db } = t.nr + helper.runInTransaction(agent, (tx) => { + db.createIndex(COLLECTIONS.collection1, 'foo', function createdIndex(error, result) { + assert.equal(error, undefined) + assert.equal(result, 'foo_1', 'should have right result') + verifyMongoSegments({ + t, + tx, + expectedSegments: ['Datastore/operation/MongoDB/createIndex', 'Callback: createdIndex'] + }) + tx.end() + end() + }) + }) + }) +}) + +test('dropCollection', async (t) => { + t.beforeEach(beforeEach) + t.afterEach(afterEach) + + await t.test('without transaction', (t, end) => { + const { agent, db } = t.nr + db.dropCollection(COLLECTIONS.collection1, (error, result) => { + assert.equal(error, undefined) + assert.equal(result, true, 'result should be boolean true') + assert.equal(agent.getTransaction(), undefined, 'should not have transaction') + end() + }) + }) + + await t.test('with transaction', (t, end) => { + const { agent, db } = t.nr + helper.runInTransaction(agent, (tx) => { + db.dropCollection(COLLECTIONS.collection1, function droppedCollection(error, result) { + assert.equal(error, undefined, 'should not have error dropping collection') + assert.equal(result, true, 'result should be boolean true') + verifyMongoSegments({ + t, + tx, + expectedSegments: [ + 'Datastore/operation/MongoDB/dropCollection', + 'Callback: droppedCollection' + ] + }) + tx.end() + end() + }) + }) + }) +}) + +test('dropDatabase', async (t) => { + t.beforeEach(beforeEach) + t.afterEach(afterEach) + + await t.test('without transaction', (t, end) => { + const { agent, db } = t.nr + db.dropDatabase((error, result) => { + assert.equal(error, undefined) + assert.equal(result, true, 'result should be boolean true') + assert.equal(agent.getTransaction(), undefined, 'should not have transaction') + end() + }) + }) + + await t.test('with transaction', (t, end) => { + const { agent, db } = t.nr + helper.runInTransaction(agent, (tx) => { + db.dropDatabase(function droppedDatabase(error, result) { + assert.equal(error, undefined, 'should not have error dropping collection') + assert.equal(result, true, 'result should be boolean true') + verifyMongoSegments({ + t, + tx, + expectedSegments: [ + 'Datastore/operation/MongoDB/dropDatabase', + 'Callback: droppedDatabase' + ] + }) + tx.end() + end() + }) + }) + }) +}) + +test('indexInformation', async (t) => { + t.beforeEach(beforeEach) + t.afterEach(afterEach) + + await t.test('without transaction', (t, end) => { + const { agent, db } = t.nr + db.createIndex(COLLECTIONS.collection1, 'foo', (error) => { + assert.equal(error, undefined, 'createIndex should not have error') + db.indexInformation(COLLECTIONS.collection1, (error, result) => { + assert.equal(error, undefined, 'indexInformation should not have error') + assert.deepStrictEqual( + result, + { _id_: [['_id', 1]], foo_1: [['foo', 1]] }, + 'result is the expected object' + ) + assert.equal(agent.getTransaction(), undefined, 'should not have transaction') + end() + }) + }) + }) + + await t.test('with transaction', (t, end) => { + const { agent, db } = t.nr + helper.runInTransaction(agent, (tx) => { + db.createIndex(COLLECTIONS.collection1, 'foo', function createdIndex(error) { + assert.equal(error, undefined, 'createIndex should not have error') + db.indexInformation(COLLECTIONS.collection1, function gotInfo(error, result) { + assert.equal(error, undefined, 'indexInformation should not have error') + assert.deepStrictEqual( + result, + { _id_: [['_id', 1]], foo_1: [['foo', 1]] }, + 'result is the expected object' + ) + verifyMongoSegments({ + t, + tx, + expectedSegments: [ + 'Datastore/operation/MongoDB/createIndex', + 'Callback: createdIndex', + 'Datastore/operation/MongoDB/indexInformation', + 'Callback: gotInfo' + ] + }) + tx.end() + end() + }) + }) + }) + }) +}) + +test('renameCollection', async (t) => { + t.beforeEach(beforeEach) + t.afterEach(afterEach) + + await t.test('without transaction', (t, end) => { + const { agent, db, mongodb } = t.nr + dropTestCollections(mongodb) + .then(() => { + db.createCollection(COLLECTIONS.collection1, function gotCollection(error) { + assert.equal(error, undefined, 'should not have error getting collection') + db.renameCollection( + COLLECTIONS.collection1, + COLLECTIONS.collection2, + function renamedCollection(error) { + assert.equal(error, undefined) + db.dropCollection(COLLECTIONS.collection2, function droppedCollection(error) { + assert.equal(error, undefined) + assert.equal(agent.getTransaction(), undefined, 'should not have transaction') + end() + }) + } + ) + }) + }) + .catch(assert.ifError) + }) + + await t.test('with transaction', (t, end) => { + const { agent, db, mongodb } = t.nr + dropTestCollections(mongodb) + .then(() => { + helper.runInTransaction(agent, (tx) => { + db.createCollection(COLLECTIONS.collection1, function gotCollection(error) { + assert.equal(error, undefined, 'should not have error getting collection') + db.renameCollection( + COLLECTIONS.collection1, + COLLECTIONS.collection2, + function renamedCollection(error) { + assert.equal(error, undefined) + db.dropCollection(COLLECTIONS.collection2, function droppedCollection(error) { + assert.equal(error, undefined) + verifyMongoSegments({ + t, + tx, + expectedSegments: [ + 'Datastore/operation/MongoDB/createCollection', + 'Callback: gotCollection', + 'Datastore/operation/MongoDB/renameCollection', + 'Callback: renamedCollection', + 'Datastore/operation/MongoDB/dropCollection', + 'Callback: droppedCollection' + ] + }) + tx.end() + end() + }) + } + ) + }) + }) + }) + .catch(assert.ifError) + }) +}) + +test('stats', async (t) => { + t.beforeEach(beforeEach) + t.afterEach(afterEach) + + await t.test('without transaction', (t, end) => { + const { agent, db } = t.nr + db.stats({}, (error, stats) => { + assert.equal(error, undefined) + matchObject(stats, { db: DB_NAME, collections: 1, ok: 1 }) + assert.equal(agent.getTransaction(), undefined, 'should not have transaction') + end() + }) + }) + + await t.test('with transaction', (t, end) => { + const { agent, db } = t.nr + helper.runInTransaction(agent, (tx) => { + db.stats(function gotStats(error, stats) { + assert.equal(error, undefined) + matchObject(stats, { db: DB_NAME, collections: 1, ok: 1 }) + verifyMongoSegments({ + t, + tx, + expectedSegments: ['Datastore/operation/MongoDB/stats', 'Callback: gotStats'] + }) + tx.end() + end() + }) + }) + }) +}) + +function verifyMongoSegments({ t, tx, expectedSegments }) { + const { agent, METRIC_HOST_NAME, METRIC_HOST_PORT } = t.nr + assert.notEqual(agent.getTransaction(), undefined, 'should not lose transaction state') + assert.equal(agent.getTransaction().id, tx.id, 'transaction is correct') + + const segment = agent.tracer.getSegment() + let current = tx.trace.root + + for (let i = 0, l = expectedSegments.length; i < l; i += 1) { + // Filter out net.createConnection segments as they could occur during + // execution, and we don't need to verify them. + current.children = current.children.filter((c) => c.name !== 'net.createConnection') + assert.equal(current.children.length, 1, 'should have one child segment') + current = current.children[0] + assert.equal( + current.name, + expectedSegments[i], + `segment should be named ${expectedSegments[i]}` + ) + + // If this is a Mongo operation/statement segment then it should have the + // datastore instance attributes. + if (/^Datastore\/.*?\/MongoDB/.test(current.name) === true) { + if (isBadSegment(current) === true) { + t.diagnostic(`skipping attributes check for ${current.name}`) + continue + } + + // Commands, known as "admin commands", always happen against the "admin" + // database regardless of the DB the connection is actually connected to. + // This is apparently by design. + // htps://jira.mongodb.org/browse/NODE-827 + let dbName = DB_NAME + if (/\/renameCollection$/.test(current.name) === true) { + dbName = 'admin' + } + + const attributes = current.getAttributes() + assert.equal(attributes.database_name, dbName, 'should have correct db name') + assert.equal(attributes.host, METRIC_HOST_NAME, 'should have correct host name') + assert.equal(attributes.port_path_or_id, METRIC_HOST_PORT, 'should have correct port') + assert.equal(attributes.product, 'MongoDB', 'should have correct product attribute') + } + } + + assert.equal(current, segment, `current segment is ${segment.name}`) +} + +function isBadSegment(segment) { + const nameParts = segment.name.split('/') + const command = nameParts.at(-1) + const attributes = segment.getAttributes() + return ( + BAD_MONGO_COMMANDS.indexOf(command) !== -1 && // Is in the list of bad commands. + !attributes.database_name && // and does not have any of the + !attributes.host && // instance attributes + !attributes.port_path_or_id + ) +} diff --git a/test/versioned/mongodb-esm/package.json b/test/versioned/mongodb-esm/package.json index 9111d54909..a6809ce822 100644 --- a/test/versioned/mongodb-esm/package.json +++ b/test/versioned/mongodb-esm/package.json @@ -1,25 +1,24 @@ { "name": "mongodb-esm-tests", - "targets": [{"name":"mongodb","minAgentVersion":"1.32.0"}], "version": "0.0.0", "type": "module", "private": true, "tests": [ { "engines": { - "node": ">=16.12.0" + "node": ">=18" }, "dependencies": { "mongodb": ">= 4.1.4 < 5" }, "files": [ - "bulk.tap.mjs", - "collection-find.tap.mjs", - "collection-index.tap.mjs", - "collection-misc.tap.mjs", - "collection-update.tap.mjs", - "cursor.tap.mjs", - "db.tap.mjs" + "bulk.test.mjs", + "collection-find.test.mjs", + "collection-index.test.mjs", + "collection-misc.test.mjs", + "collection-update.test.mjs", + "cursor.test.mjs", + "db.test.mjs" ] } ], diff --git a/test/versioned/mongodb-esm/test-assertions.mjs b/test/versioned/mongodb-esm/test-assertions.mjs new file mode 100644 index 0000000000..75b85690b0 --- /dev/null +++ b/test/versioned/mongodb-esm/test-assertions.mjs @@ -0,0 +1,163 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'node:assert' +import common from '../mongodb/common.js' + +const TRANSACTION_NAME = 'mongo test' +const { DB_NAME, STATEMENT_PREFIX } = common.ESM + +export { getValidatorCallback, matchObject } + +function getValidatorCallback({ t, tx, segments, metrics, end, childrenLength = 1 }) { + const { agent, METRIC_HOST_NAME, METRIC_HOST_PORT } = t.nr + return function done(error) { + assert.equal(error, undefined) + assert.equal(agent.getTransaction().id, tx.id, 'should not change transactions') + + const segment = agent.tracer.getSegment() + let current = tx.trace.root + + if (childrenLength === 2) { + // This block is for testing `collection.aggregate`. The `aggregate` + // method does not return a callback with a cursor, it only returns a + // cursor. So the segments on the transaction are not nested. They are + // both on the trace root. Instead of traversing the children, we iterate + // over the expected segments and compare against the corresponding child + // on the trace root. We also added a strict flag for `aggregate` because, + // depending on the version, there is an extra segment for the callback + // of our test which we do not need to assert. + assert.equal(current.children.length, childrenLength, 'should have two children') + for (const [i, expectedSegment] of segments.entries()) { + const child = current.children[i] + assert.equal(child.name, expectedSegment, `child should be named ${expectedSegment}`) + if (common.MONGO_SEGMENT_RE.test(child.name) === true) { + checkSegmentParams(child, METRIC_HOST_NAME, METRIC_HOST_PORT) + assert.equal(child.ignore, false, 'should not ignore segment') + } + assert.equal(child.children.length, 0, 'should have no more children') + } + } else { + for (let i = 0, l = segments.length; i < l; ++i) { + assert.equal(current.children.length, 1, 'should have one child') + current = current.children[0] + assert.equal(current.name, segments[i], 'child should be named ' + segments[i]) + if (common.MONGO_SEGMENT_RE.test(current.name) === true) { + checkSegmentParams(current, METRIC_HOST_NAME, METRIC_HOST_PORT) + assert.equal(current.ignore, false, 'should not ignore segment') + } + } + assert.equal(current.children.length, 0, 'should have no more children') + } + assert.equal(current === segment, true, 'should test to the current segment') + + tx.end() + checkMetrics({ + t, + agent, + host: METRIC_HOST_NAME, + port: METRIC_HOST_PORT, + metrics, + prefix: STATEMENT_PREFIX + }) + + end() + } +} + +function checkSegmentParams(segment, host, port) { + let dbName = DB_NAME + if (/\/rename$/.test(segment.name) === true) { + dbName = 'admin' + } + + const attributes = segment.getAttributes() + assert.equal(attributes.database_name, dbName, 'should have correct db name') + assert.equal(attributes.host, host, 'should have correct host name') + assert.equal(attributes.port_path_or_id, port, 'should have correct port') +} + +function checkMetrics({ agent, host, port, metrics = [], prefix = STATEMENT_PREFIX }) { + const agentMetrics = agent.metrics._metrics + const unscopedMetrics = agentMetrics.unscoped + const unscopedDatastoreNames = Object.keys(unscopedMetrics).filter((k) => k.includes('Datastore')) + const scoped = agentMetrics.scoped[TRANSACTION_NAME] + let total = 0 + + assert.notEqual(scoped, undefined, 'should have scoped metrics') + assert.equal(Object.keys(agentMetrics.scoped).length, 1, 'should have one metric scope') + for (let i = 0; i < metrics.length; ++i) { + let count = null + let name = null + + if (Array.isArray(metrics[i]) === true) { + count = metrics[i][1] + name = metrics[i][0] + } else { + count = 1 + name = metrics[i] + } + total += count + + assert.equal( + unscopedMetrics['Datastore/operation/MongoDB/' + name].callCount, + count, + `unscoped operation metrics should be called ${count} times` + ) + assert.equal( + unscopedMetrics[`${prefix}/${name}`].callCount, + count, + `unscoped statement metric should be called ${count} times` + ) + assert.equal( + scoped[`${prefix}/${name}`].callCount, + count, + `scoped statement metrics should be called ${count} times` + ) + } + + let expectedUnscopedCount = 5 + 2 * metrics.length + if (agent.config.security.agent.enabled === true) { + // The security agent adds a `Supportability/API/instrumentDatastore` metric + // via `API.prototype.instrumentDatastore`. + expectedUnscopedCount += 1 + } + assert.equal( + unscopedDatastoreNames.length, + expectedUnscopedCount, + `should have ${expectedUnscopedCount} unscoped metrics` + ) + + const expectedUnscopedMetrics = [ + 'Datastore/all', + 'Datastore/allWeb', + 'Datastore/MongoDB/all', + 'Datastore/MongoDB/allWeb', + 'Datastore/instance/MongoDB/' + host + '/' + port + ] + for (const metric of expectedUnscopedMetrics) { + assert.notEqual(unscopedMetrics[metric], undefined, `should have unscoped metric ${metric}`) + assert.equal(unscopedMetrics[metric].callCount, total, 'should have correct call count') + } +} + +function matchObject(obj, expected) { + for (const key of Object.keys(expected)) { + if (Object.prototype.toString.call(obj[key]) === '[object Object]') { + matchObject(obj[key], expected[key]) + continue + } + if (Array.isArray(obj[key]) === true) { + // Do a simple element count check until we need something deeper. + assert.equal( + obj[key].length, + expected[key].length, + `array ${key} should have same number of elements` + ) + continue + } + assert.equal(obj[key], expected[key], `${key} should equal ${expected[key]}`) + } +} diff --git a/test/versioned/mongodb-esm/test-hooks.mjs b/test/versioned/mongodb-esm/test-hooks.mjs new file mode 100644 index 0000000000..37640c414f --- /dev/null +++ b/test/versioned/mongodb-esm/test-hooks.mjs @@ -0,0 +1,75 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +// This file provides the `beforeEach` and `afterEach` hooks that every +// suite requires in order to set up and teardown the database. + +import helper from '../../lib/agent_helper.js' +import common from '../mongodb/common.js' + +const { DB_NAME, COLLECTIONS } = common.ESM + +export { beforeEach, afterEach, dropTestCollections } + +async function beforeEach(ctx) { + ctx.nr = {} + ctx.nr.agent = helper.instrumentMockedAgent() + + const { default: mongodb } = await import('mongodb') + ctx.nr.mongodb = mongodb + + await dropTestCollections(mongodb) + ctx.nr.METRIC_HOST_NAME = common.getHostName(ctx.nr.agent) + ctx.nr.METRIC_HOST_PORT = common.getPort() + const conn = await common.connect({ mongodb, name: DB_NAME }) + ctx.nr.client = conn.client + ctx.nr.db = conn.db + ctx.nr.collection = conn.db.collection(COLLECTIONS.collection1) + await populate(conn.db, ctx.nr.collection) +} + +async function afterEach(ctx) { + helper.unloadAgent(ctx.nr.agent) + await common.close(ctx.nr.client, ctx.nr.db) +} + +async function populate(db, collection) { + const items = [] + for (let i = 0; i < 30; ++i) { + items.push({ + i: i, + next3: [i + 1, i + 2, i + 3], + data: Math.random().toString(36).slice(2), + mod10: i % 10, + // spiral out + loc: [i % 4 && (i + 1) % 4 ? i : -i, (i + 1) % 4 && (i + 2) % 4 ? i : -i] + }) + } + + await collection.deleteMany({}) + await collection.insert(items) +} + +/** + * Bootstrap a running MongoDB instance by dropping all the collections used + * by tests. + * @param {*} mongodb MongoDB module to execute commands on. + */ +async function dropTestCollections(mongodb) { + const collections = Object.values(COLLECTIONS) + const { client, db } = await common.connect({ mongodb, name: DB_NAME }) + + const dbCollections = (await db.listCollections().toArray()).map((c) => c.name) + for (const collection of collections) { + if (dbCollections.includes(collection) === false) { + continue + } + try { + await db.dropCollection(collection) + } catch {} + } + + await common.close(client, db) +} diff --git a/test/versioned/mongodb/common.js b/test/versioned/mongodb/common.js index dee7cb35b6..429ecea3a3 100644 --- a/test/versioned/mongodb/common.js +++ b/test/versioned/mongodb/common.js @@ -115,8 +115,9 @@ function checkMetrics({ t, agent, host, port, metrics = [], prefix = STATEMENT_P } let expectedUnscopedCount = 5 + 2 * metrics.length - // adds a supportability metric to load k2 mongodb instrumentation if (agent.config.security.agent.enabled) { + // The security agent adds a `Supportability/API/instrumentDatastore` metric + // via `API.prototype.instrumentDatastore`. expectedUnscopedCount += 1 } t.equal(