Skip to content

Commit 07cb5b7

Browse files
test(NODE-4363): Add support for ClientEncryption in the unified test runner (#3314)
1 parent 3c5bcb9 commit 07cb5b7

File tree

11 files changed

+766
-34
lines changed

11 files changed

+766
-34
lines changed

src/encrypter.ts

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { MONGO_CLIENT_EVENTS } from './constants';
44
import type { AutoEncrypter, AutoEncryptionOptions } from './deps';
55
import { MongoInvalidArgumentError, MongoMissingDependencyError } from './error';
66
import { MongoClient, MongoClientOptions } from './mongo_client';
7-
import type { Callback } from './utils';
7+
import { Callback, getMongoDBClientEncryption } from './utils';
88

99
let AutoEncrypterClass: AutoEncrypter;
1010

@@ -123,23 +123,15 @@ export class Encrypter {
123123
}
124124

125125
static checkForMongoCrypt(): void {
126-
let mongodbClientEncryption = undefined;
127-
// Ensure you always wrap an optional require in the try block NODE-3199
128126
try {
129-
// Note (NODE-4254): This is to get around the circular dependency between
130-
// mongodb-client-encryption and the driver in the test scenarios.
131-
if (process.env.MONGODB_CLIENT_ENCRYPTION_OVERRIDE) {
132-
mongodbClientEncryption = require(process.env.MONGODB_CLIENT_ENCRYPTION_OVERRIDE);
133-
} else {
134-
mongodbClientEncryption = require('mongodb-client-encryption');
135-
}
136-
} catch (err) {
127+
// NOTE(NODE-3199): Ensure you always wrap an optional require in the try block
128+
const mongodbClientEncryption = getMongoDBClientEncryption();
129+
AutoEncrypterClass = mongodbClientEncryption.extension(require('../lib/index')).AutoEncrypter;
130+
} catch {
137131
throw new MongoMissingDependencyError(
138132
'Auto-encryption requested, but the module is not installed. ' +
139133
'Please add `mongodb-client-encryption` as a dependency of your project'
140134
);
141135
}
142-
143-
AutoEncrypterClass = mongodbClientEncryption.extension(require('../lib/index')).AutoEncrypter;
144136
}
145137
}

src/utils.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1407,3 +1407,26 @@ export function commandSupportsReadConcern(command: Document, options?: Document
14071407

14081408
return false;
14091409
}
1410+
1411+
/**
1412+
* A utility function to get the instance of mongodb-client-encryption, if it exists.
1413+
*
1414+
* @throws MongoMissingDependencyError if mongodb-client-encryption isn't installed.
1415+
* @returns
1416+
*/
1417+
export function getMongoDBClientEncryption() {
1418+
let mongodbClientEncryption;
1419+
1420+
// NOTE(NODE-4254): This is to get around the circular dependency between
1421+
// mongodb-client-encryption and the driver in the test scenarios.
1422+
if (
1423+
typeof process.env.MONGODB_CLIENT_ENCRYPTION_OVERRIDE === 'string' &&
1424+
process.env.MONGODB_CLIENT_ENCRYPTION_OVERRIDE.length > 0
1425+
) {
1426+
mongodbClientEncryption = require(process.env.MONGODB_CLIENT_ENCRYPTION_OVERRIDE);
1427+
} else {
1428+
mongodbClientEncryption = require('mongodb-client-encryption');
1429+
}
1430+
1431+
return mongodbClientEncryption;
1432+
}

test/integration/client-side-encryption/client_side_encryption.spec.test.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
TestRunnerContext
88
} from '../../tools/spec-runner';
99
import { runUnifiedSuite } from '../../tools/unified-spec-runner/runner';
10+
import { TestFilter } from '../../tools/unified-spec-runner/schema';
1011

1112
const isAuthEnabled = process.env.AUTH === 'auth';
1213

@@ -80,8 +81,19 @@ describe('Client Side Encryption (Legacy)', function () {
8081
});
8182

8283
describe('Client Side Encryption (Unified)', function () {
83-
runUnifiedSuite(
84-
loadSpecTests(path.join('client-side-encryption', 'tests', 'unified')),
85-
() => 'NODE-4330 - implement the key management API'
86-
);
84+
const filter: TestFilter = ({ description }) => {
85+
if (
86+
description.includes('create datakey with') ||
87+
description.includes('create data key with') ||
88+
description.includes('rewrap')
89+
) {
90+
if (description === 'no keys to rewrap due to no filter matches') {
91+
return 'TODO(NODE-4330): implement the key management API';
92+
}
93+
return false;
94+
}
95+
96+
return 'TODO(NODE-4330): implement the key management API';
97+
};
98+
runUnifiedSuite(loadSpecTests(path.join('client-side-encryption', 'tests', 'unified')), filter);
8799
});

test/integration/unified-test-format/unified_test_format.spec.test.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,6 @@ const filter: TestFilter = ({ description }) => {
3737
return 'TODO(NODE-3891): fix tests broken when AUTH enabled';
3838
}
3939

40-
if (description.length === 0) {
41-
// This may seem weird, but the kmsProvider valid pass tests really test that the new
42-
// client encryption entity is constructed correctly so the "test" section of each
43-
// unified test is empty (save the required properties) and the test description
44-
// is just the empty string
45-
return 'TODO(NODE-4363): add support for client encryption entity to unified runner';
46-
}
47-
4840
return false;
4941
};
5042

test/readme.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,12 @@ The following steps will walk you through how to run the tests for CSFLE.
402402
set -e CREDS
403403
```
404404

405+
1. If you are running the unified tests, you must set the following environment variable as well:
406+
407+
```shell
408+
export TEST_CSFLE=true
409+
```
410+
405411
1. Run the functional tests:
406412

407413
`npm run check:test`

test/tools/unified-spec-runner/entities.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,13 @@ import { WriteConcern } from '../../../src/write_concern';
3535
import { ejson, getEnvironmentalOptions } from '../../tools/utils';
3636
import type { TestConfiguration } from '../runner/config';
3737
import { trace } from './runner';
38-
import type { ClientEntity, EntityDescription } from './schema';
39-
import { makeConnectionString, patchCollectionOptions, patchDbOptions } from './unified-utils';
38+
import type { ClientEncryption, ClientEntity, EntityDescription } from './schema';
39+
import {
40+
createClientEncryption,
41+
makeConnectionString,
42+
patchCollectionOptions,
43+
patchDbOptions
44+
} from './unified-utils';
4045

4146
export interface UnifiedChangeStream extends ChangeStream {
4247
eventCollector: InstanceType<typeof import('../../tools/utils')['EventCollector']>;
@@ -231,6 +236,7 @@ export type Entity =
231236
| AbstractCursor
232237
| UnifiedChangeStream
233238
| GridFSBucket
239+
| ClientEncryption
234240
| Document; // Results from operations
235241

236242
export type EntityCtor =
@@ -240,7 +246,8 @@ export type EntityCtor =
240246
| typeof ClientSession
241247
| typeof ChangeStream
242248
| typeof AbstractCursor
243-
| typeof GridFSBucket;
249+
| typeof GridFSBucket
250+
| ClientEncryption;
244251

245252
export type EntityTypeId =
246253
| 'client'
@@ -249,7 +256,8 @@ export type EntityTypeId =
249256
| 'session'
250257
| 'bucket'
251258
| 'cursor'
252-
| 'stream';
259+
| 'stream'
260+
| 'clientEncryption';
253261

254262
const ENTITY_CTORS = new Map<EntityTypeId, EntityCtor>();
255263
ENTITY_CTORS.set('client', UnifiedMongoClient);
@@ -275,6 +283,7 @@ export class EntitiesMap<E = Entity> extends Map<string, E> {
275283
mapOf(type: 'bucket'): EntitiesMap<GridFSBucket>;
276284
mapOf(type: 'cursor'): EntitiesMap<AbstractCursor>;
277285
mapOf(type: 'stream'): EntitiesMap<UnifiedChangeStream>;
286+
mapOf(type: 'clientEncryption'): EntitiesMap<ClientEncryption>;
278287
mapOf(type: EntityTypeId): EntitiesMap<Entity> {
279288
const ctor = ENTITY_CTORS.get(type);
280289
if (!ctor) {
@@ -290,12 +299,17 @@ export class EntitiesMap<E = Entity> extends Map<string, E> {
290299
getEntity(type: 'bucket', key: string, assertExists?: boolean): GridFSBucket;
291300
getEntity(type: 'cursor', key: string, assertExists?: boolean): AbstractCursor;
292301
getEntity(type: 'stream', key: string, assertExists?: boolean): UnifiedChangeStream;
302+
getEntity(type: 'clientEncryption', key: string, assertExists?: boolean): ClientEncryption;
293303
getEntity(type: EntityTypeId, key: string, assertExists = true): Entity {
294304
const entity = this.get(key);
295305
if (!entity) {
296306
if (assertExists) throw new Error(`Entity '${key}' does not exist`);
297307
return;
298308
}
309+
if (type === 'clientEncryption') {
310+
// we do not have instanceof checking here since csfle might not be installed
311+
return entity;
312+
}
299313
const ctor = ENTITY_CTORS.get(type);
300314
if (!ctor) {
301315
throw new Error(`Unknown type ${type}`);
@@ -423,6 +437,10 @@ export class EntitiesMap<E = Entity> extends Map<string, E> {
423437
map.set(entity.bucket.id, new GridFSBucket(db, options));
424438
} else if ('stream' in entity) {
425439
throw new Error(`Unsupported Entity ${JSON.stringify(entity)}`);
440+
} else if ('clientEncryption' in entity) {
441+
const clientEncryption = createClientEncryption(map, entity.clientEncryption);
442+
443+
map.set(entity.clientEncryption.id, clientEncryption);
426444
} else {
427445
throw new Error(`Unsupported Entity ${JSON.stringify(entity)}`);
428446
}

test/tools/unified-spec-runner/match.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ export function resultCheck(
204204

205205
if (checkExtraKeys) {
206206
expect(actual, `Expected actual to exist at ${path.join('')}`).to.exist;
207+
// by using `Object.keys`, we ignore non-enumerable properties. This is intentional.
207208
const actualKeys = Object.keys(actual);
208209
const expectedKeys = Object.keys(expected);
209210
// Don't check for full key set equality because some of the actual keys

test/tools/unified-spec-runner/operations.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,76 @@ operations.set('rename', async ({ entities, operation }) => {
464464
return collection.rename(to, options);
465465
});
466466

467+
operations.set('createDataKey', async ({ entities, operation }) => {
468+
const clientEncryption = entities.getEntity('clientEncryption', operation.object);
469+
const { kmsProvider, opts } = operation.arguments ?? {};
470+
471+
return clientEncryption.createDataKey(kmsProvider, opts);
472+
});
473+
474+
operations.set('rewrapManyDataKey', async ({ entities, operation }) => {
475+
const clientEncryption = entities.getEntity('clientEncryption', operation.object);
476+
const { filter, opts } = operation.arguments ?? {};
477+
478+
const rewrapManyDataKeyResult = await clientEncryption.rewrapManyDataKey(filter, opts);
479+
480+
if (rewrapManyDataKeyResult.bulkWriteResult != null) {
481+
// TODO(NODE-4393): refactor BulkWriteResult to not have a 'result' property
482+
//
483+
// The unified spec runner match function will assert that documents have no extra
484+
// keys. For `rewrapManyDataKey` operations, our unifed tests will fail because
485+
// our BulkWriteResult class has an extra property - "result". We explicitly make it
486+
// non-enumerable for the purposes of testing so that the tests can pass.
487+
const { bulkWriteResult } = rewrapManyDataKeyResult;
488+
Object.defineProperty(bulkWriteResult, 'result', {
489+
value: bulkWriteResult.result,
490+
enumerable: false
491+
});
492+
}
493+
return rewrapManyDataKeyResult;
494+
});
495+
496+
operations.set('deleteKey', async ({ entities, operation }) => {
497+
const clientEncryption = entities.getEntity('clientEncryption', operation.object);
498+
const { id } = operation.arguments ?? {};
499+
500+
return clientEncryption.deleteKey(id);
501+
});
502+
503+
operations.set('getKey', async ({ entities, operation }) => {
504+
const clientEncryption = entities.getEntity('clientEncryption', operation.object);
505+
const { id } = operation.arguments ?? {};
506+
507+
return clientEncryption.getKey(id);
508+
});
509+
510+
operations.set('getKeys', async ({ entities, operation }) => {
511+
const clientEncryption = entities.getEntity('clientEncryption', operation.object);
512+
513+
return clientEncryption.getKeys();
514+
});
515+
516+
operations.set('addKeyAltName', async ({ entities, operation }) => {
517+
const clientEncryption = entities.getEntity('clientEncryption', operation.object);
518+
const { id, keyAltName } = operation.arguments ?? {};
519+
520+
return clientEncryption.addKeyAltName(id, keyAltName);
521+
});
522+
523+
operations.set('removeKeyAltName', async ({ entities, operation }) => {
524+
const clientEncryption = entities.getEntity('clientEncryption', operation.object);
525+
const { id, keyAltName } = operation.arguments ?? {};
526+
527+
return clientEncryption.removeKeyAltName(id, keyAltName);
528+
});
529+
530+
operations.set('getKeyByAltName', async ({ entities, operation }) => {
531+
const clientEncryption = entities.getEntity('clientEncryption', operation.object);
532+
const { keyAltName } = operation.arguments ?? {};
533+
534+
return clientEncryption.getKeyByAltName(keyAltName);
535+
});
536+
467537
export async function executeOperationAndCheck(
468538
operation: OperationDescription,
469539
entities: EntitiesMap,

test/tools/unified-spec-runner/schema.ts

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ServerApiVersion } from '../../../src';
1+
import { MongoClient, ServerApiVersion } from '../../../src';
22
import type { Document, ObjectId } from '../../../src/bson';
33
import type { ReadConcernLevel } from '../../../src/read_concern';
44
import type { ReadPreferenceMode } from '../../../src/read_preference';
@@ -57,7 +57,15 @@ export const OperationNames = [
5757
'runCommand',
5858
'updateMany',
5959
'updateOne',
60-
'rename'
60+
'rename',
61+
'createDataKey',
62+
'rewrapManyDataKey',
63+
'deleteKey',
64+
'getKey',
65+
'getKeys',
66+
'addKeyAltName',
67+
'removeKeyAltName',
68+
'getKeyByAltName'
6169
] as const;
6270
export type OperationName = typeof OperationNames[number];
6371

@@ -95,6 +103,7 @@ export interface RunOnRequirement {
95103
minServerVersion?: string;
96104
topologies?: TopologyId[];
97105
serverParameters?: Document;
106+
csfle?: boolean;
98107
}
99108
export type ObservableCommandEventId =
100109
| 'commandStartedEvent'
@@ -146,13 +155,53 @@ export interface StreamEntity {
146155
id: string;
147156
hexBytes: string;
148157
}
158+
159+
export type StringOrPlaceholder = string | { $$placeholder: number };
160+
161+
export interface ClientEncryptionEntity {
162+
id: string;
163+
clientEncryptionOpts: {
164+
/** this is the id of the client entity to use as the keyvault client */
165+
keyVaultClient: string;
166+
keyVaultNamespace: string;
167+
kmsProviders: {
168+
aws?: {
169+
accessKeyId: StringOrPlaceholder;
170+
secretAccessKey: StringOrPlaceholder;
171+
sessionToken: StringOrPlaceholder;
172+
};
173+
azure?: {
174+
tenantId: StringOrPlaceholder;
175+
clientId: StringOrPlaceholder;
176+
clientSecret: StringOrPlaceholder;
177+
identityPlatformEndpoint: StringOrPlaceholder;
178+
};
179+
gcp?: {
180+
email: StringOrPlaceholder;
181+
privateKey: StringOrPlaceholder;
182+
endPoint: StringOrPlaceholder;
183+
};
184+
kmip?: {
185+
endpoint: StringOrPlaceholder;
186+
};
187+
local?: {
188+
key: StringOrPlaceholder;
189+
};
190+
};
191+
};
192+
}
193+
194+
export type KMSProvidersEntity = ClientEncryptionEntity['clientEncryptionOpts']['kmsProviders'];
195+
149196
export type EntityDescription =
150197
| { client: ClientEntity }
151198
| { database: DatabaseEntity }
152199
| { collection: CollectionEntity }
153200
| { bucket: BucketEntity }
154201
| { stream: StreamEntity }
155-
| { session: SessionEntity };
202+
| { session: SessionEntity }
203+
| { clientEncryption: ClientEncryptionEntity };
204+
156205
export interface ServerApi {
157206
version: ServerApiVersion;
158207
strict?: boolean;
@@ -241,3 +290,20 @@ export interface ExpectedError {
241290
* A type that represents the test filter provided to the unifed runner.
242291
*/
243292
export type TestFilter = (test: Test) => string | false;
293+
294+
/**
295+
* This interface represents the bare minimum of type information needed to get *some* type
296+
* safety on the client encryption object in unified tests.
297+
*/
298+
export interface ClientEncryption {
299+
// eslint-disable-next-line @typescript-eslint/no-misused-new
300+
new (client: MongoClient, options: any): ClientEncryption;
301+
createDataKey(provider, options): Promise<any>;
302+
rewrapManyDataKey(filter, options): Promise<any>;
303+
deleteKey(id): Promise<any>;
304+
getKey(id): Promise<any>;
305+
getKeys(): Promise<any>;
306+
addKeyAltName(id, keyAltName): Promise<any>;
307+
removeKeyAltName(id, keyAltName): Promise<any>;
308+
getKeyByAltName(keyAltName): Promise<any>;
309+
}

0 commit comments

Comments
 (0)