diff --git a/test/versioned/mongodb-esm/bulk.tap.mjs b/test/versioned/mongodb-esm/bulk.tap.mjs new file mode 100644 index 0000000000..e01f75a30d --- /dev/null +++ b/test/versioned/mongodb-esm/bulk.tap.mjs @@ -0,0 +1,80 @@ +/* + * 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/collection-common.mjs b/test/versioned/mongodb-esm/collection-common.mjs new file mode 100644 index 0000000000..49a369fa3a --- /dev/null +++ b/test/versioned/mongodb-esm/collection-common.mjs @@ -0,0 +1,210 @@ +/* + * 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 new file mode 100644 index 0000000000..6b0dd0f61a --- /dev/null +++ b/test/versioned/mongodb-esm/collection-find.tap.mjs @@ -0,0 +1,85 @@ +/* + * 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-index.tap.mjs b/test/versioned/mongodb-esm/collection-index.tap.mjs new file mode 100644 index 0000000000..4cde25d2c1 --- /dev/null +++ b/test/versioned/mongodb-esm/collection-index.tap.mjs @@ -0,0 +1,91 @@ +/* + * 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-misc.tap.mjs b/test/versioned/mongodb-esm/collection-misc.tap.mjs new file mode 100644 index 0000000000..2f7cb78f93 --- /dev/null +++ b/test/versioned/mongodb-esm/collection-misc.tap.mjs @@ -0,0 +1,170 @@ +/* + * 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-update.tap.mjs b/test/versioned/mongodb-esm/collection-update.tap.mjs new file mode 100644 index 0000000000..5aab4f724b --- /dev/null +++ b/test/versioned/mongodb-esm/collection-update.tap.mjs @@ -0,0 +1,212 @@ +/* + * 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/common.cjs b/test/versioned/mongodb-esm/common.cjs new file mode 100644 index 0000000000..75142cff3c --- /dev/null +++ b/test/versioned/mongodb-esm/common.cjs @@ -0,0 +1,8 @@ +/* + * Copyright 2020 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +module.exports = require('../mongodb/common') diff --git a/test/versioned/mongodb-esm/cursor.tap.mjs b/test/versioned/mongodb-esm/cursor.tap.mjs new file mode 100644 index 0000000000..fb9f7a4c00 --- /dev/null +++ b/test/versioned/mongodb-esm/cursor.tap.mjs @@ -0,0 +1,60 @@ +/* + * 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/db.tap.mjs b/test/versioned/mongodb-esm/db.tap.mjs new file mode 100644 index 0000000000..b193a2361d --- /dev/null +++ b/test/versioned/mongodb-esm/db.tap.mjs @@ -0,0 +1,323 @@ +/* + * 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/newrelic.cjs b/test/versioned/mongodb-esm/newrelic.cjs new file mode 100644 index 0000000000..5bfe53711f --- /dev/null +++ b/test/versioned/mongodb-esm/newrelic.cjs @@ -0,0 +1,25 @@ +/* + * Copyright 2020 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +exports.config = { + app_name: ['My Application'], + license_key: 'license key here', + logging: { + level: 'trace', + filepath: '../../../newrelic_agent.log' + }, + utilization: { + detect_aws: false, + detect_pcf: false, + detect_azure: false, + detect_gcp: false, + detect_docker: false + }, + transaction_tracer: { + enabled: true + } +} diff --git a/test/versioned/mongodb-esm/package.json b/test/versioned/mongodb-esm/package.json new file mode 100644 index 0000000000..9111d54909 --- /dev/null +++ b/test/versioned/mongodb-esm/package.json @@ -0,0 +1,27 @@ +{ + "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" + }, + "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" + ] + } + ], + "dependencies": {} +} diff --git a/test/versioned/mongodb/collection-common.js b/test/versioned/mongodb/collection-common.js index 6f18c96e5b..24749d8173 100644 --- a/test/versioned/mongodb/collection-common.js +++ b/test/versioned/mongodb/collection-common.js @@ -38,7 +38,7 @@ function collectionTest(name, run) { await dropTestCollections(mongodb) METRIC_HOST_NAME = common.getHostName(agent) METRIC_HOST_PORT = common.getPort() - const res = await common.connect(mongodb) + const res = await common.connect({ mongodb }) client = res.client db = res.db collection = db.collection(COLLECTIONS.collection1) @@ -124,7 +124,13 @@ function collectionTest(name, run) { } transaction.end() - common.checkMetrics(t, agent, METRIC_HOST_NAME, METRIC_HOST_PORT, metrics || []) + common.checkMetrics({ + t, + agent, + host: METRIC_HOST_NAME, + port: METRIC_HOST_PORT, + metrics + }) t.end() } ) @@ -192,7 +198,7 @@ function collectionTest(name, run) { await dropTestCollections(mongodb) METRIC_HOST_NAME = common.getHostName(agent) METRIC_HOST_PORT = common.getPort() - const res = await common.connect(mongodb, null, true) + const res = await common.connect({ mongodb, replicaSet: true }) client = res.client db = res.db collection = db.collection(COLLECTIONS.collection1) @@ -269,7 +275,13 @@ function collectionTest(name, run) { } transaction.end() - common.checkMetrics(t, agent, METRIC_HOST_NAME, METRIC_HOST_PORT, metrics || []) + common.checkMetrics({ + t, + agent, + host: METRIC_HOST_NAME, + port: METRIC_HOST_PORT, + metrics + }) t.end() } ) @@ -314,7 +326,7 @@ async function populate(collection) { */ async function dropTestCollections(mongodb) { const collections = Object.values(COLLECTIONS) - const { client, db } = await common.connect(mongodb) + const { client, db } = await common.connect({ mongodb }) const dropCollectionPromises = collections.map(async (collection) => { try { diff --git a/test/versioned/mongodb/common.js b/test/versioned/mongodb/common.js index d4dd44fb22..e4e3c0fb62 100644 --- a/test/versioned/mongodb/common.js +++ b/test/versioned/mongodb/common.js @@ -14,6 +14,12 @@ const TRANSACTION_NAME = 'mongo test' const DB_NAME = 'integration' const COLLECTIONS = { collection1: 'testCollection', collection2: 'testCollection2' } const STATEMENT_PREFIX = `Datastore/statement/MongoDB/${COLLECTIONS.collection1}` +const ESM = { + DB_NAME: 'esmIntegration', + COLLECTIONS: { collection1: 'esmTestCollection', collection2: 'esmTestCollection2' }, + STATEMENT_PREFIX: 'Datastore/statement/MongoDB/esmTestCollection' +} +exports.ESM = ESM exports.MONGO_SEGMENT_RE = MONGO_SEGMENT_RE exports.TRANSACTION_NAME = TRANSACTION_NAME @@ -28,7 +34,7 @@ exports.checkMetrics = checkMetrics exports.getHostName = getHostName exports.getPort = getPort -async function connect(mongodb, host, replicaSet = false) { +async function connect({ mongodb, host, replicaSet = false, name = DB_NAME }) { if (host) { host = encodeURIComponent(host) } else { @@ -43,7 +49,7 @@ async function connect(mongodb, host, replicaSet = false) { options = { useNewUrlParser: true, useUnifiedTopology: true } } const client = await mongodb.MongoClient.connect(connString, options) - const db = client.db(DB_NAME) + const db = client.db(name) return { db, client } } @@ -64,7 +70,7 @@ function getPort() { return String(params.mongodb_port) } -function checkMetrics(t, agent, host, port, metrics) { +function checkMetrics({ t, agent, host, port, metrics = [], prefix = STATEMENT_PREFIX }) { const agentMetrics = getMetrics(agent) const unscopedMetrics = agentMetrics.unscoped @@ -99,12 +105,12 @@ function checkMetrics(t, agent, host, port, metrics) { 'unscoped operation metric should be called ' + count + ' times' ) t.equal( - unscopedMetrics[`${STATEMENT_PREFIX}/` + name].callCount, + unscopedMetrics[`${prefix}/` + name].callCount, count, 'unscoped statement metric should be called ' + count + ' times' ) t.equal( - scoped[`${STATEMENT_PREFIX}/` + name].callCount, + scoped[`${prefix}/` + name].callCount, count, 'scoped statement metric should be called ' + count + ' times' ) diff --git a/test/versioned/mongodb/db-common.js b/test/versioned/mongodb/db-common.js index 000db19280..ff8a474b42 100644 --- a/test/versioned/mongodb/db-common.js +++ b/test/versioned/mongodb/db-common.js @@ -32,7 +32,7 @@ function dbTest(name, run) { MONGO_HOST = common.getHostName(agent) MONGO_PORT = common.getPort() - const res = await common.connect(mongodb) + const res = await common.connect({ mongodb }) client = res.client db = res.db })