From 19ba74a5eab88536f3dcf0736f770c8c20de7a0e Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Fri, 29 Jan 2021 11:07:00 -0500 Subject: [PATCH] test: Operations needed for versioned API tests (#2717) Adds operations to the unified test runner. NODE-2287 --- test/functional/unified-spec-runner/match.ts | 61 +++++++- .../unified-spec-runner/operations.ts | 147 +++++++++++++++--- test/functional/unified-spec-runner/schema.ts | 4 +- 3 files changed, 183 insertions(+), 29 deletions(-) diff --git a/test/functional/unified-spec-runner/match.ts b/test/functional/unified-spec-runner/match.ts index b9781ed7cc..9735063ac8 100644 --- a/test/functional/unified-spec-runner/match.ts +++ b/test/functional/unified-spec-runner/match.ts @@ -1,13 +1,13 @@ import { expect } from 'chai'; import { isDeepStrictEqual } from 'util'; -import { Binary, Document, Long, ObjectId } from '../../../src'; +import { Binary, Document, Long, ObjectId, MongoError } from '../../../src'; import { CommandFailedEvent, CommandStartedEvent, CommandSucceededEvent } from '../../../src/cmap/events'; import { CommandEvent, EntitiesMap } from './entities'; -import { ExpectedEvent } from './schema'; +import { ExpectedError, ExpectedEvent } from './schema'; export interface ExistsOperator { $$exists: boolean; @@ -242,3 +242,60 @@ export function matchesEvents( } } } + +export function expectErrorCheck( + error: Error | MongoError, + expected: ExpectedError, + entities: EntitiesMap +): boolean { + if (Object.keys(expected)[0] === 'isClientError' || Object.keys(expected)[0] === 'isError') { + // FIXME: We cannot tell if Error arose from driver and not from server + return; + } + + if (expected.errorContains) { + if (error.message.includes(expected.errorContains)) { + throw new Error( + `Error message was supposed to contain '${expected.errorContains}' but had '${error.message}'` + ); + } + } + + if (!(error instanceof MongoError)) { + throw new Error(`Assertions need ${error} to be a MongoError`); + } + + if (expected.errorCode) { + if (error.code !== expected.errorCode) { + throw new Error(`${error} was supposed to have code '${expected.errorCode}'`); + } + } + + if (expected.errorCodeName) { + if (error.codeName !== expected.errorCodeName) { + throw new Error(`${error} was supposed to have '${expected.errorCodeName}' codeName`); + } + } + + if (expected.errorLabelsContain) { + for (const errorLabel of expected.errorLabelsContain) { + if (!error.hasErrorLabel(errorLabel)) { + throw new Error(`${error} was supposed to have '${errorLabel}'`); + } + } + } + + if (expected.errorLabelsOmit) { + for (const errorLabel of expected.errorLabelsOmit) { + if (error.hasErrorLabel(errorLabel)) { + throw new Error(`${error} was not supposed to have '${errorLabel}'`); + } + } + } + + if (expected.expectResult) { + if (!expectResultCheck(error, expected.expectResult, entities)) { + throw new Error(`${error} supposed to match result ${JSON.stringify(expected.expectResult)}`); + } + } +} diff --git a/test/functional/unified-spec-runner/operations.ts b/test/functional/unified-spec-runner/operations.ts index 5878872224..43e7851114 100644 --- a/test/functional/unified-spec-runner/operations.ts +++ b/test/functional/unified-spec-runner/operations.ts @@ -1,18 +1,13 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { expect } from 'chai'; -import { ChangeStream, Document, InsertOneOptions, MongoError } from '../../../src'; +import { Collection, Db } from '../../../src'; +import { ChangeStream, Document, InsertOneOptions } from '../../../src'; +import { BulkWriteResult } from '../../../src/bulk/common'; import { EventCollector } from '../../tools/utils'; import { EntitiesMap } from './entities'; -import { expectResultCheck } from './match'; +import { expectErrorCheck, expectResultCheck } from './match'; import type * as uni from './schema'; -export class UnifiedOperation { - name: string; - constructor(op: uni.OperationDescription) { - this.name = op.name; - } -} - async function abortTransactionOperation( entities: EntitiesMap, op: uni.OperationDescription @@ -23,7 +18,22 @@ async function aggregateOperation( entities: EntitiesMap, op: uni.OperationDescription ): Promise { - throw new Error('not implemented.'); + const dbOrCollection = entities.get(op.object) as Db | Collection; + if (!(dbOrCollection instanceof Db || dbOrCollection instanceof Collection)) { + throw new Error(`Operation object '${op.object}' must be a db or collection`); + } + return dbOrCollection + .aggregate(op.arguments.pipeline, { + allowDiskUse: op.arguments.allowDiskUse, + batchSize: op.arguments.batchSize, + bypassDocumentValidation: op.arguments.bypassDocumentValidation, + maxTimeMS: op.arguments.maxTimeMS, + maxAwaitTimeMS: op.arguments.maxAwaitTimeMS, + collation: op.arguments.collation, + hint: op.arguments.hint, + out: op.arguments.out + }) + .toArray(); } async function assertCollectionExistsOperation( entities: EntitiesMap, @@ -94,14 +104,16 @@ async function assertSessionTransactionStateOperation( async function bulkWriteOperation( entities: EntitiesMap, op: uni.OperationDescription -): Promise { - throw new Error('not implemented.'); +): Promise { + const collection = entities.getEntity('collection', op.object); + return collection.bulkWrite(op.arguments.requests); } async function commitTransactionOperation( entities: EntitiesMap, op: uni.OperationDescription ): Promise { - throw new Error('not implemented.'); + const session = entities.getEntity('session', op.object); + return session.commitTransaction(); } async function createChangeStreamOperation( entities: EntitiesMap, @@ -148,7 +160,8 @@ async function deleteOneOperation( entities: EntitiesMap, op: uni.OperationDescription ): Promise { - throw new Error('not implemented.'); + const collection = entities.getEntity('collection', op.object); + return collection.deleteOne(op.arguments.filter); } async function dropCollectionOperation( entities: EntitiesMap, @@ -168,19 +181,24 @@ async function findOperation( ): Promise { const collection = entities.getEntity('collection', op.object); const { filter, sort, batchSize, limit } = op.arguments; - return await collection.find(filter, { sort, batchSize, limit }).toArray(); + return collection.find(filter, { sort, batchSize, limit }).toArray(); } async function findOneAndReplaceOperation( entities: EntitiesMap, op: uni.OperationDescription ): Promise { - throw new Error('not implemented.'); + const collection = entities.getEntity('collection', op.object); + return collection.findOneAndReplace(op.arguments.filter, op.arguments.replacement); } async function findOneAndUpdateOperation( entities: EntitiesMap, op: uni.OperationDescription ): Promise { - throw new Error('not implemented.'); + const collection = entities.getEntity('collection', op.object); + const returnOriginal = op.arguments.returnDocument === 'Before'; + return ( + await collection.findOneAndUpdate(op.arguments.filter, op.arguments.update, { returnOriginal }) + ).value; } async function failPointOperation( entities: EntitiesMap, @@ -201,7 +219,7 @@ async function insertOneOperation( session } as InsertOneOptions; - return await collection.insertOne(op.arguments.document, options); + return collection.insertOne(op.arguments.document, options); } async function insertManyOperation( entities: EntitiesMap, @@ -216,7 +234,7 @@ async function insertManyOperation( ordered: op.arguments.ordered ?? true }; - return await collection.insertMany(op.arguments.documents, options); + return collection.insertMany(op.arguments.documents, options); } async function iterateUntilDocumentOrErrorOperation( entities: EntitiesMap, @@ -239,13 +257,20 @@ async function replaceOneOperation( entities: EntitiesMap, op: uni.OperationDescription ): Promise { - throw new Error('not implemented.'); + const collection = entities.getEntity('collection', op.object); + return collection.replaceOne(op.arguments.filter, op.arguments.replacement, { + bypassDocumentValidation: op.arguments.bypassDocumentValidation, + collation: op.arguments.collation, + hint: op.arguments.hint, + upsert: op.arguments.upsert + }); } async function startTransactionOperation( entities: EntitiesMap, op: uni.OperationDescription -): Promise { - throw new Error('not implemented.'); +): Promise { + const session = entities.getEntity('session', op.object); + session.startTransaction(); } async function targetedFailPointOperation( entities: EntitiesMap, @@ -277,8 +302,67 @@ async function withTransactionOperation( ): Promise { throw new Error('not implemented.'); } +async function countDocumentsOperation( + entities: EntitiesMap, + op: uni.OperationDescription +): Promise { + const collection = entities.getEntity('collection', op.object); + return collection.countDocuments(op.arguments.filter as Document); +} +async function deleteManyOperation( + entities: EntitiesMap, + op: uni.OperationDescription +): Promise { + const collection = entities.getEntity('collection', op.object); + return collection.deleteMany(op.arguments.filter); +} +async function distinctOperation( + entities: EntitiesMap, + op: uni.OperationDescription +): Promise { + const collection = entities.getEntity('collection', op.object); + return collection.distinct(op.arguments.fieldName as string, op.arguments.filter as Document); +} +async function estimatedDocumentCountOperation( + entities: EntitiesMap, + op: uni.OperationDescription +): Promise { + const collection = entities.getEntity('collection', op.object); + return collection.estimatedDocumentCount(); +} +async function findOneAndDeleteOperation( + entities: EntitiesMap, + op: uni.OperationDescription +): Promise { + const collection = entities.getEntity('collection', op.object); + return collection.findOneAndDelete(op.arguments.filter); +} +async function runCommandOperation( + entities: EntitiesMap, + op: uni.OperationDescription +): Promise { + const db = entities.getEntity('db', op.object); + return db.command(op.arguments.command); +} +async function updateManyOperation( + entities: EntitiesMap, + op: uni.OperationDescription +): Promise { + const collection = entities.getEntity('collection', op.object); + return collection.updateMany(op.arguments.filter, op.arguments.update); +} +async function updateOneOperation( + entities: EntitiesMap, + op: uni.OperationDescription +): Promise { + const collection = entities.getEntity('collection', op.object); + return collection.updateOne(op.arguments.filter, op.arguments.update); +} -type RunOperationFn = (entities: EntitiesMap, op: uni.OperationDescription) => Promise; +type RunOperationFn = ( + entities: EntitiesMap, + op: uni.OperationDescription +) => Promise; export const operations = new Map(); operations.set('abortTransaction', abortTransactionOperation); @@ -321,6 +405,16 @@ operations.set('download', downloadOperation); operations.set('upload', uploadOperation); operations.set('withTransaction', withTransactionOperation); +// Versioned API adds these: +operations.set('countDocuments', countDocumentsOperation); +operations.set('deleteMany', deleteManyOperation); +operations.set('distinct', distinctOperation); +operations.set('estimatedDocumentCount', estimatedDocumentCountOperation); +operations.set('findOneAndDelete', findOneAndDeleteOperation); +operations.set('runCommand', runCommandOperation); +operations.set('updateMany', updateManyOperation); +operations.set('updateOne', updateOneOperation); + export async function executeOperationAndCheck( operation: uni.OperationDescription, entities: EntitiesMap @@ -333,9 +427,12 @@ export async function executeOperationAndCheck( try { result = await opFunc(entities, operation); } catch (error) { + // FIXME: Remove when project is done: + if (error.message === 'not implemented.') { + throw error; + } if (operation.expectError) { - expect(error).to.be.instanceof(MongoError); - // expectErrorCheck(error, operation.expectError); + expectErrorCheck(error, operation.expectError, entities); } else { expect.fail(`Operation ${operation.name} failed with ${error.message}`); } diff --git a/test/functional/unified-spec-runner/schema.ts b/test/functional/unified-spec-runner/schema.ts index 90680f81b3..caadd877cf 100644 --- a/test/functional/unified-spec-runner/schema.ts +++ b/test/functional/unified-spec-runner/schema.ts @@ -140,7 +140,7 @@ export interface ExpectedError { errorContains?: string; errorCode?: number; errorCodeName?: string; - errorLabelsContain?: [string, ...string[]]; - errorLabelsOmit?: [string, ...string[]]; + errorLabelsContain?: string[]; + errorLabelsOmit?: string[]; expectResult?: unknown; }