diff --git a/global.d.ts b/global.d.ts index 6976c9647d..5ba7f1d686 100644 --- a/global.d.ts +++ b/global.d.ts @@ -20,6 +20,7 @@ declare global { idmsMockServer?: true; nodejs?: string; predicate?: (test?: Mocha.Test) => true | string; + crypt_shared?: 'enabled' | 'disabled' }; sessions?: { diff --git a/test/integration/client-side-encryption/client_side_encryption.prose.20.mongocryptd_client.test.ts b/test/integration/client-side-encryption/client_side_encryption.prose.20.mongocryptd_client.test.ts new file mode 100644 index 0000000000..98bd88a9bd --- /dev/null +++ b/test/integration/client-side-encryption/client_side_encryption.prose.20.mongocryptd_client.test.ts @@ -0,0 +1,75 @@ +import { expect } from 'chai'; +import { once } from 'events'; +import { createServer, type Server } from 'net'; + +import { getCSFLEKMSProviders } from '../../csfle-kms-providers'; +import { type MongoClient } from '../../mongodb'; +import { getEncryptExtraOptions } from '../../tools/utils'; + +describe('20. Bypass creating mongocryptd client when shared library is loaded', function () { + let server: Server; + let hasConnection = false; + let client: MongoClient; + + beforeEach(function () { + // Start a new thread (referred to as listenerThread) + // On listenerThread, create a TcpListener on 127.0.0.1 endpoint and port 27021. Start the listener and wait for establishing connections. If any connection is established, then signal about this to the main thread. + // Drivers MAY pass a different port if they expect their testing infrastructure to be using port 27021. Pass a port that should be free. + // In Node, we don't need to create a separate thread for the server. + server = createServer({}); + server.listen(27021); + server.on('connection', () => (hasConnection = true)); + + // Create a MongoClient configured with auto encryption (referred to as client_encrypted) + // Configure the required options. Use the local KMS provider as follows: + // { "local": { "key": } } + // Configure with the keyVaultNamespace set to keyvault.datakeys. + // Configure the following extraOptions: + // { + // "mongocryptdURI": "mongodb://localhost:27021/?serverSelectionTimeoutMS=1000" + // } + client = this.configuration.newClient( + {}, + { + autoEncryption: { + kmsProviders: { local: getCSFLEKMSProviders().local }, + keyVaultNamespace: 'keyvault.datakeys', + extraOptions: { + cryptSharedLibPath: getEncryptExtraOptions().cryptSharedLibPath, + mongocryptdURI: 'mongodb://localhost:27021' + } + } + } + ); + }); + + afterEach(async function () { + server && (await once(server.close(), 'close')); + await client?.close(); + }); + + it( + 'does not create or use a mongocryptd client when the shared library is loaded', + { + requires: { + clientSideEncryption: true, + crypt_shared: 'enabled' + } + }, + async function () { + // Use client_encrypted to insert the document {"unencrypted": "test"} into db.coll. + await client.db('db').collection('coll').insertOne({ unencrypted: 'test' }); + + // Expect no signal from listenerThread. + expect(hasConnection).to.be.false; + + // Note: this assertion is not in the spec test. However, unlike other drivers, Node's client + // does not connect when instantiated. So, we won't receive any TCP connections to the + // server if the mongocryptd client is only instantiated. This assertion captures the + // spirit of this test, causing it to fail if we do instantiate a client. I left the + // TCP server in, although it isn't necessary for Node's test, just because its nice to have + // in case Node's client behavior ever changes. + expect(client.autoEncrypter._mongocryptdClient).to.be.undefined; + } + ); +}); diff --git a/test/integration/client-side-encryption/client_side_encryption.prose.test.js b/test/integration/client-side-encryption/client_side_encryption.prose.test.js index 7d3c265997..60ed2231b2 100644 --- a/test/integration/client-side-encryption/client_side_encryption.prose.test.js +++ b/test/integration/client-side-encryption/client_side_encryption.prose.test.js @@ -40,6 +40,7 @@ const getKmsProviders = (localKey, kmipEndpoint, azureEndpoint, gcpEndpoint) => }; const noop = () => {}; +/** @type { MongoDBMetadataUI } */ const metadata = { requires: { clientSideEncryption: true, @@ -1146,47 +1147,43 @@ describe('Client Side Encryption Prose Tests', metadata, function () { ); }); - beforeEach('precondition: the shared library must NOT be loaded', function () { - const { cryptSharedLibPath } = getEncryptExtraOptions(); - if (cryptSharedLibPath) { - this.currentTest.skipReason = - 'test requires that the shared library NOT is present, but CRYPT_SHARED_LIB_PATH is set.'; - this.skip(); - } - // the presence of the shared library can only be reliably determine after - // libmongocrypt has been initialized, and can be detected with the - // cryptSharedLibVersionInfo getter on the autoEncrypter. - expect(!!clientEncrypted.autoEncrypter.cryptSharedLibVersionInfo).to.be.false; - }); - afterEach(async function () { await clientEncrypted?.close(); }); - it('does not spawn mongocryptd', metadata, async function () { - // Use client_encrypted to insert the document {"encrypted": "test"} into db.coll. - // Expect a server selection error propagated from the internal MongoClient failing to connect to mongocryptd on port 27021. - const insertError = await clientEncrypted - .db(dataDbName) - .collection(dataCollName) - .insertOne({ encrypted: 'test' }) - .catch(e => e); + it( + 'does not spawn mongocryptd', + { + requires: { + ...metadata.requires, + crypt_shared: 'enabled' + } + }, + async function () { + // Use client_encrypted to insert the document {"encrypted": "test"} into db.coll. + // Expect a server selection error propagated from the internal MongoClient failing to connect to mongocryptd on port 27021. + const insertError = await clientEncrypted + .db(dataDbName) + .collection(dataCollName) + .insertOne({ encrypted: 'test' }) + .catch(e => e); - expect(insertError) - .to.be.instanceOf(MongoRuntimeError) - .to.match( - /Unable to connect to `mongocryptd`, please make sure it is running or in your PATH for auto-spawn/ - ); + expect(insertError) + .to.be.instanceOf(MongoRuntimeError) + .to.match( + /Unable to connect to `mongocryptd`, please make sure it is running or in your PATH for auto-spawn/ + ); - const { cause } = insertError; + const { cause } = insertError; - expect(cause).to.be.instanceOf(MongoServerSelectionError); - expect(cause, 'Error must contain ECONNREFUSED').to.satisfy( - error => - /ECONNREFUSED/.test(error.message) || - !!error.cause?.cause?.errors?.every(e => e.code === 'ECONNREFUSED') - ); - }); + expect(cause).to.be.instanceOf(MongoServerSelectionError); + expect(cause, 'Error must contain ECONNREFUSED').to.satisfy( + error => + /ECONNREFUSED/.test(error.message) || + !!error.cause?.cause?.errors?.every(e => e.code === 'ECONNREFUSED') + ); + } + ); }); describe('via bypassAutoEncryption', function () { @@ -1241,19 +1238,6 @@ describe('Client Side Encryption Prose Tests', metadata, function () { expect(insertResult).to.have.property('insertedId'); }); - beforeEach('precondition: the shared library must NOT be loaded', function () { - const { cryptSharedLibPath } = getEncryptExtraOptions(); - if (cryptSharedLibPath) { - this.currentTest.skipReason = - 'test requires that the shared library NOT is present, but CRYPT_SHARED_LIB_PATH is set.'; - this.skip(); - } - // the presence of the shared library can only be reliably determine after - // libmongocrypt has been initialized, and can be detected with the - // cryptSharedLibVersionInfo getter on the autoEncrypter. - expect(!!clientEncrypted.autoEncrypter.cryptSharedLibVersionInfo).to.be.false; - }); - afterEach(async function () { await clientEncrypted?.close(); await client?.close(); @@ -1262,34 +1246,34 @@ describe('Client Side Encryption Prose Tests', metadata, function () { // Validate that mongocryptd was not spawned. Create a MongoClient to localhost:27021 // (or whatever was passed via --port) with serverSelectionTimeoutMS=1000. Run a handshake // command and ensure it fails with a server selection timeout. - it('does not spawn mongocryptd', metadata, async function () { - client = new MongoClient('mongodb://localhost:27021/db?serverSelectionTimeoutMS=1000'); - const error = await client.connect().catch(e => e); + it( + 'does not spawn mongocryptd', + { + requires: { + ...metadata.requires, + crypt_shared: 'enabled' + } + }, + async function () { + client = new MongoClient('mongodb://localhost:27021/db?serverSelectionTimeoutMS=1000'); + const error = await client.connect().catch(e => e); - expect(error, 'Error MUST be a MongoServerSelectionError error').to.be.instanceOf( - MongoServerSelectionError - ); - expect(error, 'Error MUST contain ECONNREFUSED information').to.satisfy( - error => - /ECONNREFUSED/.test(error.message) || - !!error.cause?.cause?.errors?.every(e => e.code === 'ECONNREFUSED') - ); - }); + expect(error, 'Error MUST be a MongoServerSelectionError error').to.be.instanceOf( + MongoServerSelectionError + ); + expect(error, 'Error MUST contain ECONNREFUSED information').to.satisfy( + error => + /ECONNREFUSED/.test(error.message) || + !!error.cause?.cause?.errors?.every(e => e.code === 'ECONNREFUSED') + ); + } + ); }); describe('via loading shared library', function () { let clientEncrypted; let client; - beforeEach(function () { - const { cryptSharedLibPath } = getEncryptExtraOptions(); - if (!cryptSharedLibPath) { - this.currentTest.skipReason = - 'test requires that the shared library is present, but CRYPT_SHARED_LIB_PATH is unset.'; - this.skip(); - } - }); - // Setup beforeEach(async function () { const { cryptSharedLibPath } = getEncryptExtraOptions(); @@ -1345,11 +1329,23 @@ describe('Client Side Encryption Prose Tests', metadata, function () { // 4. Validate that mongocryptd was not spawned. Create a MongoClient to localhost:27021 (or // whatever was passed via `--port` with serverSelectionTimeoutMS=1000.) Run a handshake // command and ensure it fails with a server selection timeout - it('should not spawn mongocryptd', metadata, async function () { - client = new MongoClient('mongodb://localhost:27021/db?serverSelectionTimeoutMS=1000'); - const error = await client.connect().catch(e => e); - expect(error).to.be.instanceOf(MongoServerSelectionError, /'Server selection timed out'/i); - }); + it( + 'should not spawn mongocryptd', + { + requires: { + ...metadata.requires, + crypt_shared: 'enabled' + } + }, + async function () { + client = new MongoClient('mongodb://localhost:27021/db?serverSelectionTimeoutMS=1000'); + const error = await client.connect().catch(e => e); + expect(error).to.be.instanceOf( + MongoServerSelectionError, + /'Server selection timed out'/i + ); + } + ); }); }); diff --git a/test/integration/node-specific/auto_encrypter.test.ts b/test/integration/node-specific/auto_encrypter.test.ts index babd192f0c..4dbbbfd8c7 100644 --- a/test/integration/node-specific/auto_encrypter.test.ts +++ b/test/integration/node-specific/auto_encrypter.test.ts @@ -10,17 +10,6 @@ import { StateMachine, type UUID } from '../../mongodb'; -import { ClientSideEncryptionFilter } from '../../tools/runner/filters/client_encryption_filter'; - -export const cryptShared = (status: 'enabled' | 'disabled') => () => { - const isCryptSharedLoaded = ClientSideEncryptionFilter.cryptShared != null; - - if (status === 'enabled') { - return isCryptSharedLoaded ? true : 'Test requires the shared library.'; - } - - return isCryptSharedLoaded ? 'Test requires that the crypt shared library NOT be present' : true; -}; describe('mongocryptd auto spawn', function () { let client: MongoClient; @@ -75,7 +64,7 @@ describe('mongocryptd auto spawn', function () { it( 'should autoSpawn a mongocryptd on init by default', - { requires: { clientSideEncryption: true, predicate: cryptShared('disabled') } }, + { requires: { clientSideEncryption: true, crypt_shared: 'disabled' } }, async function () { const autoEncrypter = client.autoEncrypter; const mongocryptdManager = autoEncrypter._mongocryptdManager; @@ -90,7 +79,7 @@ describe('mongocryptd auto spawn', function () { it( 'should not attempt to kick off mongocryptd on a non-network error from mongocrpytd', - { requires: { clientSideEncryption: true, predicate: cryptShared('disabled') } }, + { requires: { clientSideEncryption: true, crypt_shared: 'disabled' } }, async function () { let called = false; sinon @@ -124,7 +113,7 @@ describe('mongocryptd auto spawn', function () { it( 'should respawn the mongocryptd after a MongoNetworkTimeoutError is returned when communicating with mongocryptd', - { requires: { clientSideEncryption: true, predicate: cryptShared('disabled') } }, + { requires: { clientSideEncryption: true, crypt_shared: 'disabled' } }, async function () { let called = false; sinon @@ -158,7 +147,7 @@ describe('mongocryptd auto spawn', function () { it( 'should propagate error if MongoNetworkTimeoutError is experienced twice in a row', - { requires: { clientSideEncryption: true, predicate: cryptShared('disabled') } }, + { requires: { clientSideEncryption: true, crypt_shared: 'disabled' } }, async function () { const stub = sinon .stub(StateMachine.prototype, 'markCommand') @@ -193,7 +182,7 @@ describe('mongocryptd auto spawn', function () { it( 'should return a useful message if mongocryptd fails to autospawn', - { requires: { clientSideEncryption: true, predicate: cryptShared('disabled') } }, + { requires: { clientSideEncryption: true, crypt_shared: 'disabled' } }, async function () { client = this.configuration.newClient( {}, diff --git a/test/integration/node-specific/crypt_shared_lib.test.ts b/test/integration/node-specific/crypt_shared_lib.test.ts index 2bf526f91b..a3d9412aa8 100644 --- a/test/integration/node-specific/crypt_shared_lib.test.ts +++ b/test/integration/node-specific/crypt_shared_lib.test.ts @@ -4,7 +4,6 @@ import { dirname } from 'path'; import { BSON } from '../../mongodb'; import { getEncryptExtraOptions } from '../../tools/utils'; -import { cryptShared } from './auto_encrypter.test'; const { EJSON } = BSON; @@ -30,7 +29,7 @@ describe('crypt shared library', () => { 'should load a shared library by specifying its path', { requires: { - predicate: cryptShared('enabled') + crypt_shared: 'enabled' } }, async function () { @@ -57,7 +56,7 @@ describe('crypt shared library', () => { 'should load a shared library by specifying a search path', { requires: { - predicate: cryptShared('enabled') + crypt_shared: 'enabled' } }, async function () { diff --git a/test/tools/runner/config.ts b/test/tools/runner/config.ts index 46f41a02a8..69314408a3 100644 --- a/test/tools/runner/config.ts +++ b/test/tools/runner/config.ts @@ -88,6 +88,7 @@ export class TestConfiguration { version: string; libmongocrypt: string | null; }; + cryptSharedVersion: MongoClient['autoEncrypter']['cryptSharedLibVersionInfo'] | null; parameters: Record; singleMongosLoadBalancerUri: string; multiMongosLoadBalancerUri: string; @@ -121,6 +122,7 @@ export class TestConfiguration { const hostAddresses = hosts.map(HostAddress.fromString); this.version = context.version; this.clientSideEncryption = context.clientSideEncryption; + this.cryptSharedVersion = context.cryptShared; this.parameters = { ...context.parameters }; this.singleMongosLoadBalancerUri = context.singleMongosLoadBalancerUri; this.multiMongosLoadBalancerUri = context.multiMongosLoadBalancerUri; diff --git a/test/tools/runner/filters/client_encryption_filter.ts b/test/tools/runner/filters/client_encryption_filter.ts index 3bb66df72f..17c5619015 100644 --- a/test/tools/runner/filters/client_encryption_filter.ts +++ b/test/tools/runner/filters/client_encryption_filter.ts @@ -4,44 +4,9 @@ import * as process from 'process'; import { satisfies } from 'semver'; import { kmsCredentialsPresent } from '../../../csfle-kms-providers'; -import { type AutoEncrypter, MongoClient } from '../../../mongodb'; +import { type MongoClient } from '../../../mongodb'; import { Filter } from './filter'; -function getCryptSharedVersion(): AutoEncrypter['cryptSharedLibVersionInfo'] | null { - try { - const mc = new MongoClient('mongodb://localhost:27017', { - autoEncryption: { - kmsProviders: { - local: { - key: Buffer.alloc(96) - } - }, - extraOptions: { - cryptSharedLibPath: process.env.CRYPT_SHARED_LIB_PATH - } - } - }); - return mc.autoEncrypter.cryptSharedLibVersionInfo; - } catch { - try { - const mc = new MongoClient('mongodb://localhost:27017', { - autoEncryption: { - kmsProviders: { - local: { - key: Buffer.alloc(96) - } - } - } - }); - return mc.autoEncrypter.cryptSharedLibVersionInfo; - } catch { - // squash errors - } - } - - return null; -} - /** * Filter for whether or not a test needs / doesn't need Client Side Encryption * @@ -56,33 +21,32 @@ function getCryptSharedVersion(): AutoEncrypter['cryptSharedLibVersionInfo'] | n */ export class ClientSideEncryptionFilter extends Filter { - enabled: boolean; - static version = null; - static libmongocrypt: string | null = null; - static cryptShared: AutoEncrypter['cryptSharedLibVersionInfo'] | null = null; + enabled: boolean = false; + version: string | null = null; + libmongocrypt: string | null = null; override async initializeFilter(client: MongoClient, context: Record) { let mongodbClientEncryption: typeof import('mongodb-client-encryption'); try { // eslint-disable-next-line @typescript-eslint/no-require-imports mongodbClientEncryption = require('mongodb-client-encryption'); - ClientSideEncryptionFilter.libmongocrypt = ( + this.libmongocrypt = ( mongodbClientEncryption as typeof import('mongodb-client-encryption') ).MongoCrypt.libmongocryptVersion; - - ClientSideEncryptionFilter.cryptShared = getCryptSharedVersion(); } catch (failedToGetFLELib) { if (process.env.TEST_CSFLE) { console.error({ failedToGetFLELib }); } } - ClientSideEncryptionFilter.version ??= JSON.parse( - await readFile( - resolve(dirname(require.resolve('mongodb-client-encryption')), '..', 'package.json'), - 'utf8' - ) - ).version; + if (!this.version) { + this.version = JSON.parse( + await readFile( + resolve(dirname(require.resolve('mongodb-client-encryption')), '..', 'package.json'), + 'utf8' + ) + ).version; + } this.enabled = !!(kmsCredentialsPresent && mongodbClientEncryption); @@ -90,9 +54,8 @@ export class ClientSideEncryptionFilter extends Filter { context.clientSideEncryption = { enabled: this.enabled, mongodbClientEncryption, - version: ClientSideEncryptionFilter.version, - libmongocrypt: ClientSideEncryptionFilter.libmongocrypt, - cryptShared: ClientSideEncryptionFilter.cryptShared + version: this.version, + libmongocrypt: this.libmongocrypt }; } @@ -112,18 +75,17 @@ export class ClientSideEncryptionFilter extends Filter { // TODO(NODE-3401): unskip csfle tests on windows if (process.env.TEST_CSFLE && process.platform !== 'win32') { - if (ClientSideEncryptionFilter.version == null) { + if (this.version == null) { throw new Error('FLE tests must run, but mongodb client encryption was not installed.'); } } if (!kmsCredentialsPresent) return 'Test requires FLE kms credentials'; - if (ClientSideEncryptionFilter.version == null) - return 'Test requires mongodb-client-encryption to be installed.'; + if (this.version == null) return 'Test requires mongodb-client-encryption to be installed.'; const validRange = typeof clientSideEncryption === 'string' ? clientSideEncryption : '>=0.0.0'; - return satisfies(ClientSideEncryptionFilter.version, validRange, { includePrerelease: true }) + return satisfies(this.version, validRange, { includePrerelease: true }) ? true - : `requires mongodb-client-encryption ${validRange}, received ${ClientSideEncryptionFilter.version}`; + : `requires mongodb-client-encryption ${validRange}, received ${this.version}`; } } diff --git a/test/tools/runner/filters/crypt_shared_filter.ts b/test/tools/runner/filters/crypt_shared_filter.ts new file mode 100644 index 0000000000..bf82435708 --- /dev/null +++ b/test/tools/runner/filters/crypt_shared_filter.ts @@ -0,0 +1,84 @@ +import { type AutoEncrypter, MongoClient } from '../../../mongodb'; +import { getEncryptExtraOptions } from '../../utils'; +import { Filter } from './filter'; + +function getCryptSharedVersion(): AutoEncrypter['cryptSharedLibVersionInfo'] | null { + try { + const mc = new MongoClient('mongodb://localhost:27017', { + autoEncryption: { + kmsProviders: { + local: { + key: Buffer.alloc(96) + } + }, + extraOptions: getEncryptExtraOptions() + } + }); + return mc.autoEncrypter.cryptSharedLibVersionInfo; + } catch { + try { + const mc = new MongoClient('mongodb://localhost:27017', { + autoEncryption: { + kmsProviders: { + local: { + key: Buffer.alloc(96) + } + } + } + }); + return mc.autoEncrypter.cryptSharedLibVersionInfo; + } catch { + // squash errors + } + } + + return null; +} + +/** + * Filter for whether or not a test needs or does not need the crypt_shared FLE shared library. + * + * @example + * ```js + * metadata: { + * requires: { + * crypt_shared: 'enabled' | 'disabled' + * } + * } + * ``` + * + * - If `crypt_shared: 'enabled'`, the test will only run if crypt_shared is present. + * - If `crypt_shared: 'disabled'`, the test will only run if crypt_shared is not present. + * - If not specified, the test will always run. + */ +export class CryptSharedFilter extends Filter { + cryptShared: AutoEncrypter['cryptSharedLibVersionInfo'] | null = getCryptSharedVersion(); + + override async initializeFilter( + _client: MongoClient, + context: Record + ): Promise { + context.cryptSharedVersion = this.cryptShared; + } + + filter(test: { metadata?: MongoDBMetadataUI }): boolean | string { + const cryptSharedRequirement = test.metadata?.requires?.crypt_shared; + + if (cryptSharedRequirement == null) { + return true; + } + + const cryptSharedPresent = Boolean(this.cryptShared); + + if (cryptSharedRequirement === 'enabled') { + return cryptSharedPresent || 'Test requires crypt_shared to be present.'; + } + if (cryptSharedRequirement === 'disabled') { + return !cryptSharedPresent || 'Test requires crypt_shared to be absent.'; + } + + throw new Error( + "cryptShared filter only supports requires.cryptShared: 'enabled' | 'disabled'" + ); + } +} diff --git a/test/tools/runner/hooks/configuration.ts b/test/tools/runner/hooks/configuration.ts index 15172322ce..4a0ea9f499 100644 --- a/test/tools/runner/hooks/configuration.ts +++ b/test/tools/runner/hooks/configuration.ts @@ -23,6 +23,7 @@ import { OSFilter } from '../filters/os_filter'; import { type Filter } from '../filters/filter'; import { type Context } from 'mocha'; import { flakyTests } from '../flaky'; +import { CryptSharedFilter } from '../filters/crypt_shared_filter'; // Default our tests to have auth enabled // A better solution will be tackled in NODE-3714 @@ -57,6 +58,7 @@ async function initializeFilters(client): Promise> { new ApiVersionFilter(), new AuthFilter(), new ClientSideEncryptionFilter(), + new CryptSharedFilter(), new GenericPredicateFilter(), new IDMSMockServerFilter(), new MongoDBTopologyFilter(), @@ -163,6 +165,8 @@ const testConfigBeforeHook = async function () { csfle: { ...this.configuration.clientSideEncryption }, + cryptSharedVersion: context.cryptSharedVersion, + cryptSharedLibPath: process.env.CRYPT_SHARED_LIB_PATH, serverApi: MONGODB_API_VERSION, atlas: process.env.ATLAS_CONNECTIVITY != null, aws: MONGODB_URI.includes('authMechanism=MONGODB-AWS'), @@ -175,7 +179,6 @@ const testConfigBeforeHook = async function () { ldap: MONGODB_URI.includes('authMechanism=PLAIN'), socks5: MONGODB_URI.includes('proxyHost='), compressor: process.env.COMPRESSOR, - cryptSharedLibPath: process.env.CRYPT_SHARED_LIB_PATH, zstdVersion };