Skip to content
76 changes: 73 additions & 3 deletions packages/cli-repl/test/e2e-fle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { inspect } from 'util';
import path from 'path';

describe('FLE tests', () => {
const testServer = startTestServer('shared');
const testServer = startTestServer('not-shared', '--replicaset', '--nodes', '1');
skipIfServerVersion(testServer, '< 4.2'); // FLE only available on 4.2+
skipIfCommunityServer(testServer); // FLE is enterprise-only
useBinaryPath(testServer); // Get mongocryptd in the PATH for this test
Expand Down Expand Up @@ -204,10 +204,9 @@ describe('FLE tests', () => {
await shell.executeLine(`autoMongo = Mongo(${uri}, { \
keyVaultNamespace: '${dbname}.keyVault', \
kmsProviders: { local }, \
schemaMap: schemaMap \
schemaMap \
});`);


await shell.executeLine(`bypassMongo = Mongo(${uri}, { \
keyVaultNamespace: '${dbname}.keyVault', \
kmsProviders: { local }, \
Expand Down Expand Up @@ -237,6 +236,77 @@ describe('FLE tests', () => {
expect(plainMongoResult).to.not.include("phoneNumber: '+12874627836445'");
});

context('6.0+', () => {
skipIfServerVersion(testServer, '< 6.0'); // FLE2 only available on 6.0+

it('drops fle2 collection with all helper collections when encryptedFields options are in listCollections', async() => {
const shell = TestShell.start({
args: ['--nodb'],
env: {
...process.env,
MONGOSH_FLE2_SUPPORT: 'true'
},
});
const uri = JSON.stringify(await testServer.connectionString());

await shell.waitForPrompt();

await shell.executeLine('local = { key: BinData(0, "kh4Gv2N8qopZQMQYMEtww/AkPsIrXNmEMxTrs3tUoTQZbZu4msdRUaR8U5fXD7A7QXYHcEvuu4WctJLoT+NvvV3eeIg3MD+K8H9SR794m/safgRHdIfy6PD+rFpvmFbY") }');

await shell.executeLine(`keyMongo = Mongo(${uri}, { \
keyVaultNamespace: '${dbname}.keyVault', \
kmsProviders: { local } \
});`);

await shell.executeLine('keyVault = keyMongo.getKeyVault();');
await shell.executeLine('keyId = keyVault.createKey("local");');

await shell.executeLine(`encryptedFieldsMap = { \
'${dbname}.collfle2': { \
fields: [{ path: 'phoneNumber', keyId, bsonType: 'string' }] \
} \
};`);

await shell.executeLine(`autoMongo = Mongo(${uri}, { \
keyVaultNamespace: '${dbname}.keyVault', \
kmsProviders: { local }, \
encryptedFieldsMap \
});`);

// Drivers will create the auxilliary FLE2 collections only when explicitly creating collections
// via the createCollection() command.
await shell.executeLine(`autoMongo.getDB('${dbname}').createCollection('collfle2');`);
await shell.executeLine(`autoMongo.getDB('${dbname}').collfle2.insertOne({ \
phoneNumber: '+12874627836445' \
});`);

const autoMongoResult = await shell.executeLine(`autoMongo.getDB('${dbname}').collfle2.find()`);
expect(autoMongoResult).to.include("phoneNumber: '+12874627836445'");

await shell.executeLine(`plainMongo = Mongo(${uri});`);

const plainMongoResult = await shell.executeLine(`plainMongo.getDB('${dbname}').collfle2.find()`);
expect(plainMongoResult).to.include('phoneNumber: Binary(Buffer.from');
expect(plainMongoResult).to.not.include("phoneNumber: '+12874627836445'");

let collections = await shell.executeLine(`plainMongo.getDB('${dbname}').getCollectionNames()`);

expect(collections).to.include('enxcol_.collfle2.ecc');
expect(collections).to.include('enxcol_.collfle2.esc');
expect(collections).to.include('enxcol_.collfle2.ecoc');
expect(collections).to.include('collfle2');

await shell.executeLine(`plainMongo.getDB('${dbname}').collfle2.drop();`);

collections = await shell.executeLine(`plainMongo.getDB('${dbname}').getCollectionNames()`);

expect(collections).to.not.include('enxcol_.collfle2.ecc');
expect(collections).to.not.include('enxcol_.collfle2.esc');
expect(collections).to.not.include('enxcol_.collfle2.ecoc');
expect(collections).to.not.include('collfle2');
});
});

it('performs KeyVault data key management as expected', async() => {
const shell = TestShell.start({
args: [await testServer.connectionString()]
Expand Down
67 changes: 67 additions & 0 deletions packages/shell-api/src/collection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1243,6 +1243,7 @@ describe('Collection', () => {
});

it('passes through options', async() => {
serviceProvider.listCollections.resolves([{}]);
serviceProvider.dropCollection.resolves();
await collection.drop({ promoteValues: false });
expect(serviceProvider.dropCollection).to.have.been.calledWith(
Expand Down Expand Up @@ -1873,6 +1874,72 @@ describe('Collection', () => {
});
});
});
describe('fle2', () => {
let mongo1: Mongo;
let mongo2: Mongo;
let serviceProvider: StubbedInstance<ServiceProvider>;
let database: Database;
let bus: StubbedInstance<EventEmitter>;
let instanceState: ShellInstanceState;
let collection: Collection;
let keyId: any[]
;
beforeEach(() => {
bus = stubInterface<EventEmitter>();
serviceProvider = stubInterface<ServiceProvider>();
serviceProvider.runCommand.resolves({ ok: 1 });
serviceProvider.runCommandWithCheck.resolves({ ok: 1 });
serviceProvider.initialDb = 'test';
serviceProvider.bsonLibrary = bson;
instanceState = new ShellInstanceState(serviceProvider, bus);
keyId = [ { $binary: { base64: 'oh3caogGQ4Sf34ugKnZ7Xw==', subType: '04' } } ];
mongo1 = new Mongo(
instanceState,
undefined,
{
keyVaultNamespace: 'db1.keyvault',
kmsProviders: { local: { key: 'A'.repeat(128) } },
encryptedFieldsMap: {
'db1.collfle2': {
fields: [{ path: 'phoneNumber', keyId, bsonType: 'string' }],
}
}
},
undefined,
serviceProvider
);
database = new Database(mongo1, 'db1');
collection = new Collection(mongo1, database, 'collfle2');
mongo2 = new Mongo(
instanceState,
undefined,
undefined,
undefined,
serviceProvider
);
});

describe('drop', () => {
it('does not pass encryptedFields through options when collection is in encryptedFieldsMap', async() => {
serviceProvider.dropCollection.resolves();
await collection.drop();
expect(serviceProvider.dropCollection).to.have.been.calledWith(
'db1', 'collfle2', {}
);
});

it('passes encryptedFields through options when collection is not in encryptedFieldsMap', async() => {
serviceProvider.listCollections.resolves([{
options: { encryptedFields: { fields: [ { path: 'phoneNumber', keyId, bsonType: 'string' } ] } }
}]);
serviceProvider.dropCollection.resolves();
await mongo2.getDB('db1').getCollection('collfle2').drop();
expect(serviceProvider.dropCollection).to.have.been.calledWith(
'db1', 'collfle2', { encryptedFields: { fields: [ { path: 'phoneNumber', keyId, bsonType: 'string' } ] } }
);
});
});
});
describe('with session', () => {
let serviceProvider: StubbedInstance<ServiceProvider>;
let collection: Collection;
Expand Down
25 changes: 24 additions & 1 deletion packages/shell-api/src/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1340,11 +1340,34 @@ export default class Collection extends ShellApiWithMongoClass {
async drop(options: DropCollectionOptions = {}): Promise<boolean> {
this._emitCollectionApiCall('drop');

let encryptedFieldsOptions = {};

// @ts-expect-error waiting for driver release
const encryptedFieldsMap = this._mongo._fleOptions?.encryptedFieldsMap;
const encryptedFields: Document | undefined = encryptedFieldsMap?.[`${this._database._name}.${ this._name}`];

// @ts-expect-error waiting for driver release
if (!encryptedFields && !options.encryptedFields) {
const collectionInfos = await this._mongo._serviceProvider.listCollections(
this._database._name,
{
name: this._name
},
await this._database._baseOptions()
);

const encryptedFields: Document | undefined = collectionInfos?.[0]?.options?.encryptedFields;

if (encryptedFields) {
encryptedFieldsOptions = { encryptedFields };
}
}

try {
return await this._mongo._serviceProvider.dropCollection(
this._database._name,
this._name,
{ ...await this._database._baseOptions(), ...options }
{ ...await this._database._baseOptions(), ...options, ...encryptedFieldsOptions }
);
} catch (error: any) {
if (error?.codeName === 'NamespaceNotFound') {
Expand Down