From 732b0094c86dee912439bca18187fd772b0f0a13 Mon Sep 17 00:00:00 2001 From: bailey Date: Wed, 16 Jul 2025 09:59:57 -0600 Subject: [PATCH 1/7] add csfle prose test 20 --- ...yption.prose.20.mongocryptd_client.test.ts | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 test/integration/client-side-encryption/client_side_encryption.prose.20.mongocryptd_client.test.ts 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 00000000000..abb0ef2f377 --- /dev/null +++ b/test/integration/client-side-encryption/client_side_encryption.prose.20.mongocryptd_client.test.ts @@ -0,0 +1,60 @@ +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 { ClientSideEncryptionFilter } from '../../tools/runner/filters/client_encryption_filter'; +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 () { + if (!ClientSideEncryptionFilter.cryptShared) { + this.currentTest.skipReason = + 'test requires that the crypt shared be loaded into the current process.'; + this.skip(); + } + + server = createServer({}); + server.listen(27021); + server.on('connection', () => (hasConnection = true)); + + 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 + } + }, + async function () { + await client.db('db').collection('coll').insertOne({ unencrypted: 'test' }); + expect(hasConnection).to.be.false; + + expect(client.autoEncrypter._mongocryptdClient).to.be.undefined; + } + ); +}); From 2e06e53c1121449f4e9e6a898ab13dd6f4c0dd0b Mon Sep 17 00:00:00 2001 From: bailey Date: Wed, 16 Jul 2025 10:48:07 -0600 Subject: [PATCH 2/7] comment test --- ...yption.prose.20.mongocryptd_client.test.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) 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 index abb0ef2f377..7e360db6712 100644 --- 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 @@ -19,10 +19,22 @@ describe('20. Bypass creating mongocryptd client when shared library is loaded', this.skip(); } + // 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( {}, { @@ -51,9 +63,18 @@ describe('20. Bypass creating mongocryptd client when shared library is loaded', } }, 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 unless 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; } ); From e5e6b2a0990813657b62aab13584965941f9b819 Mon Sep 17 00:00:00 2001 From: bailey Date: Wed, 16 Jul 2025 15:47:28 -0600 Subject: [PATCH 3/7] add dbName to cmd events --- src/cmap/command_monitoring_events.ts | 4 +++ test/tools/unified-spec-runner/match.ts | 44 +++++++++++++++--------- test/tools/unified-spec-runner/schema.ts | 2 ++ 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/src/cmap/command_monitoring_events.ts b/src/cmap/command_monitoring_events.ts index 992b9dc47c9..b5e7c776d57 100644 --- a/src/cmap/command_monitoring_events.ts +++ b/src/cmap/command_monitoring_events.ts @@ -96,6 +96,7 @@ export class CommandSucceededEvent { commandName: string; reply: unknown; serviceId?: ObjectId; + databaseName: string; /** @internal */ name = COMMAND_SUCCEEDED; @@ -127,6 +128,7 @@ export class CommandSucceededEvent { this.duration = calculateDurationInMs(started); this.reply = maybeRedact(commandName, cmd, extractReply(reply)); this.serverConnectionId = serverConnectionId; + this.databaseName = command.databaseName; } /* @internal */ @@ -154,6 +156,7 @@ export class CommandFailedEvent { commandName: string; failure: Error; serviceId?: ObjectId; + databaseName: string; /** @internal */ name = COMMAND_FAILED; @@ -186,6 +189,7 @@ export class CommandFailedEvent { this.duration = calculateDurationInMs(started); this.failure = maybeRedact(commandName, cmd, error) as Error; this.serverConnectionId = serverConnectionId; + this.databaseName = command.databaseName; } /* @internal */ diff --git a/test/tools/unified-spec-runner/match.ts b/test/tools/unified-spec-runner/match.ts index 1b8d0d7836e..2e3c35a4dcc 100644 --- a/test/tools/unified-spec-runner/match.ts +++ b/test/tools/unified-spec-runner/match.ts @@ -461,16 +461,14 @@ function compareCommandStartedEvents( if (expected!.commandName) { expect( expected!.commandName, - `expected ${prefix}.commandName to equal ${expected!.commandName} but received ${ - actual.commandName + `expected ${prefix}.commandName to equal ${expected!.commandName} but received ${actual.commandName }` ).to.equal(actual.commandName); } if (expected!.databaseName) { expect( expected!.databaseName, - `expected ${prefix}.databaseName to equal ${expected!.databaseName} but received ${ - actual.databaseName + `expected ${prefix}.databaseName to equal ${expected!.databaseName} but received ${actual.databaseName }` ).to.equal(actual.databaseName); } @@ -478,37 +476,49 @@ function compareCommandStartedEvents( function compareCommandSucceededEvents( actual: CommandSucceededEvent, - expected: ExpectedCommandEvent['commandSucceededEvent'], + expected: NonNullable, entities: EntitiesMap, prefix: string ) { - if (expected!.reply) { - resultCheck(actual.reply as Document, expected!.reply, entities, [prefix]); + if (expected.reply) { + resultCheck(actual.reply as Document, expected.reply, entities, [prefix]); } - if (expected!.commandName) { + if (expected.commandName) { expect( - expected!.commandName, - `expected ${prefix}.commandName to equal ${expected!.commandName} but received ${ - actual.commandName + expected.commandName, + `expected ${prefix}.commandName to equal ${expected.commandName} but received ${actual.commandName }` ).to.equal(actual.commandName); } + if (expected.databaseName) { + expect( + expected.databaseName, + `expected ${prefix}.databaseName to equal ${expected.databaseName} but received ${actual.databaseName + }` + ).to.equal(actual.databaseName); + } } function compareCommandFailedEvents( actual: CommandFailedEvent, - expected: ExpectedCommandEvent['commandFailedEvent'], - entities: EntitiesMap, + expected: NonNullable, + _entities: EntitiesMap, prefix: string ) { - if (expected!.commandName) { + if (expected.commandName) { expect( - expected!.commandName, - `expected ${prefix}.commandName to equal ${expected!.commandName} but received ${ - actual.commandName + expected.commandName, + `expected ${prefix}.commandName to equal ${expected.commandName} but received ${actual.commandName }` ).to.equal(actual.commandName); } + if (expected.databaseName) { + expect( + expected.databaseName, + `expected ${prefix}.databaseName to equal ${expected.databaseName} but received ${actual.databaseName + }` + ).to.equal(actual.databaseName); + } } function expectInstanceOf any>( diff --git a/test/tools/unified-spec-runner/schema.ts b/test/tools/unified-spec-runner/schema.ts index d52192e645c..cd2bd3ff3d5 100644 --- a/test/tools/unified-spec-runner/schema.ts +++ b/test/tools/unified-spec-runner/schema.ts @@ -312,10 +312,12 @@ export interface ExpectedCommandEvent { commandSucceededEvent?: { reply?: Document; commandName?: string; + databaseName?: string; hasServerConnectionId?: boolean; }; commandFailedEvent?: { commandName?: string; + databaseName?: string; hasServerConnectionId?: boolean; }; } From 904ded11e0d80cf1e728669a25776772b788e232 Mon Sep 17 00:00:00 2001 From: bailey Date: Wed, 16 Jul 2025 15:50:47 -0600 Subject: [PATCH 4/7] sync invalid tests --- ...-commandFailedEvent-databaseName-type.json | 29 +++++++++++++++++++ ...t-commandFailedEvent-databaseName-type.yml | 16 ++++++++++ ...mmandSucceededEvent-databaseName-type.json | 29 +++++++++++++++++++ ...ommandSucceededEvent-databaseName-type.yml | 16 ++++++++++ 4 files changed, 90 insertions(+) create mode 100644 test/spec/unified-test-format/invalid/expectedCommandEvent-commandFailedEvent-databaseName-type.json create mode 100644 test/spec/unified-test-format/invalid/expectedCommandEvent-commandFailedEvent-databaseName-type.yml create mode 100644 test/spec/unified-test-format/invalid/expectedCommandEvent-commandSucceededEvent-databaseName-type.json create mode 100644 test/spec/unified-test-format/invalid/expectedCommandEvent-commandSucceededEvent-databaseName-type.yml diff --git a/test/spec/unified-test-format/invalid/expectedCommandEvent-commandFailedEvent-databaseName-type.json b/test/spec/unified-test-format/invalid/expectedCommandEvent-commandFailedEvent-databaseName-type.json new file mode 100644 index 00000000000..f6a305b89a7 --- /dev/null +++ b/test/spec/unified-test-format/invalid/expectedCommandEvent-commandFailedEvent-databaseName-type.json @@ -0,0 +1,29 @@ +{ + "description": "expectedCommandEvent-commandFailedEvent-databaseName-type", + "schemaVersion": "1.15", + "createEntities": [ + { + "client": { + "id": "client0" + } + } + ], + "tests": [ + { + "description": "foo", + "operations": [], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandFailedEvent": { + "databaseName": 0 + } + } + ] + } + ] + } + ] +} diff --git a/test/spec/unified-test-format/invalid/expectedCommandEvent-commandFailedEvent-databaseName-type.yml b/test/spec/unified-test-format/invalid/expectedCommandEvent-commandFailedEvent-databaseName-type.yml new file mode 100644 index 00000000000..9ab33ad59e2 --- /dev/null +++ b/test/spec/unified-test-format/invalid/expectedCommandEvent-commandFailedEvent-databaseName-type.yml @@ -0,0 +1,16 @@ +description: "expectedCommandEvent-commandFailedEvent-databaseName-type" + +schemaVersion: "1.15" + +createEntities: + - client: + id: &client0 "client0" + +tests: + - description: "foo" + operations: [] + expectEvents: + - client: *client0 + events: + - commandFailedEvent: + databaseName: 0 diff --git a/test/spec/unified-test-format/invalid/expectedCommandEvent-commandSucceededEvent-databaseName-type.json b/test/spec/unified-test-format/invalid/expectedCommandEvent-commandSucceededEvent-databaseName-type.json new file mode 100644 index 00000000000..47b8c8bb9d0 --- /dev/null +++ b/test/spec/unified-test-format/invalid/expectedCommandEvent-commandSucceededEvent-databaseName-type.json @@ -0,0 +1,29 @@ +{ + "description": "expectedCommandEvent-commandSucceededEvent-databaseName-type", + "schemaVersion": "1.15", + "createEntities": [ + { + "client": { + "id": "client0" + } + } + ], + "tests": [ + { + "description": "foo", + "operations": [], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandSucceededEvent": { + "databaseName": 0 + } + } + ] + } + ] + } + ] +} diff --git a/test/spec/unified-test-format/invalid/expectedCommandEvent-commandSucceededEvent-databaseName-type.yml b/test/spec/unified-test-format/invalid/expectedCommandEvent-commandSucceededEvent-databaseName-type.yml new file mode 100644 index 00000000000..94adc2b7305 --- /dev/null +++ b/test/spec/unified-test-format/invalid/expectedCommandEvent-commandSucceededEvent-databaseName-type.yml @@ -0,0 +1,16 @@ +description: "expectedCommandEvent-commandSucceededEvent-databaseName-type" + +schemaVersion: "1.15" + +createEntities: + - client: + id: &client0 "client0" + +tests: + - description: "foo" + operations: [] + expectEvents: + - client: *client0 + events: + - commandSucceededEvent: + databaseName: 0 From 0a36b573135d9d210205c19cd231011c3a34a470 Mon Sep 17 00:00:00 2001 From: bailey Date: Fri, 18 Jul 2025 14:35:53 -0600 Subject: [PATCH 5/7] unless -> if --- .../client_side_encryption.prose.20.mongocryptd_client.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 7e360db6712..5928766d5ab 100644 --- 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 @@ -71,7 +71,7 @@ describe('20. Bypass creating mongocryptd client when shared library is loaded', // 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 unless the mongocryptd client is only instantiated. This assertion captures 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. From 09856aabe16925e7117ec4b49e1bfdb09b0a612b Mon Sep 17 00:00:00 2001 From: bailey Date: Mon, 21 Jul 2025 09:50:51 -0600 Subject: [PATCH 6/7] add cyrpt_shared filter --- global.d.ts | 1 + ...yption.prose.20.mongocryptd_client.test.ts | 10 +- .../client_side_encryption.prose.test.js | 142 +++++++++--------- .../node-specific/auto_encrypter.test.ts | 21 +-- .../node-specific/crypt_shared_lib.test.ts | 5 +- test/tools/runner/config.ts | 2 + .../filters/client_encryption_filter.ts | 43 +----- .../runner/filters/crypt_shared_filter.ts | 84 +++++++++++ test/tools/runner/hooks/configuration.ts | 5 +- test/tools/unified-spec-runner/match.ts | 18 ++- 10 files changed, 183 insertions(+), 148 deletions(-) create mode 100644 test/tools/runner/filters/crypt_shared_filter.ts diff --git a/global.d.ts b/global.d.ts index 6976c9647d4..5ba7f1d6865 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 index 5928766d5ab..98bd88a9bdf 100644 --- 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 @@ -4,7 +4,6 @@ import { createServer, type Server } from 'net'; import { getCSFLEKMSProviders } from '../../csfle-kms-providers'; import { type MongoClient } from '../../mongodb'; -import { ClientSideEncryptionFilter } from '../../tools/runner/filters/client_encryption_filter'; import { getEncryptExtraOptions } from '../../tools/utils'; describe('20. Bypass creating mongocryptd client when shared library is loaded', function () { @@ -13,12 +12,6 @@ describe('20. Bypass creating mongocryptd client when shared library is loaded', let client: MongoClient; beforeEach(function () { - if (!ClientSideEncryptionFilter.cryptShared) { - this.currentTest.skipReason = - 'test requires that the crypt shared be loaded into the current process.'; - this.skip(); - } - // 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. @@ -59,7 +52,8 @@ describe('20. Bypass creating mongocryptd client when shared library is loaded', 'does not create or use a mongocryptd client when the shared library is loaded', { requires: { - clientSideEncryption: true + clientSideEncryption: true, + crypt_shared: 'enabled' } }, async function () { 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 7d3c2659973..60ed2231b22 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 babd192f0cd..4dbbbfd8c72 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 2bf526f91b6..a3d9412aa84 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 b7f9951d574..26497a304a0 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 3bb66df72fa..49347cde63a 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 * @@ -59,7 +24,6 @@ export class ClientSideEncryptionFilter extends Filter { enabled: boolean; static version = null; static libmongocrypt: string | null = null; - static cryptShared: AutoEncrypter['cryptSharedLibVersionInfo'] | null = null; override async initializeFilter(client: MongoClient, context: Record) { let mongodbClientEncryption: typeof import('mongodb-client-encryption'); @@ -69,8 +33,6 @@ export class ClientSideEncryptionFilter extends Filter { ClientSideEncryptionFilter.libmongocrypt = ( mongodbClientEncryption as typeof import('mongodb-client-encryption') ).MongoCrypt.libmongocryptVersion; - - ClientSideEncryptionFilter.cryptShared = getCryptSharedVersion(); } catch (failedToGetFLELib) { if (process.env.TEST_CSFLE) { console.error({ failedToGetFLELib }); @@ -91,8 +53,7 @@ export class ClientSideEncryptionFilter extends Filter { enabled: this.enabled, mongodbClientEncryption, version: ClientSideEncryptionFilter.version, - libmongocrypt: ClientSideEncryptionFilter.libmongocrypt, - cryptShared: ClientSideEncryptionFilter.cryptShared + libmongocrypt: ClientSideEncryptionFilter.libmongocrypt }; } 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 00000000000..bf824357081 --- /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 15172322cef..4a0ea9f4996 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 }; diff --git a/test/tools/unified-spec-runner/match.ts b/test/tools/unified-spec-runner/match.ts index 2e3c35a4dcc..06a23388c01 100644 --- a/test/tools/unified-spec-runner/match.ts +++ b/test/tools/unified-spec-runner/match.ts @@ -461,14 +461,16 @@ function compareCommandStartedEvents( if (expected!.commandName) { expect( expected!.commandName, - `expected ${prefix}.commandName to equal ${expected!.commandName} but received ${actual.commandName + `expected ${prefix}.commandName to equal ${expected!.commandName} but received ${ + actual.commandName }` ).to.equal(actual.commandName); } if (expected!.databaseName) { expect( expected!.databaseName, - `expected ${prefix}.databaseName to equal ${expected!.databaseName} but received ${actual.databaseName + `expected ${prefix}.databaseName to equal ${expected!.databaseName} but received ${ + actual.databaseName }` ).to.equal(actual.databaseName); } @@ -486,14 +488,16 @@ function compareCommandSucceededEvents( if (expected.commandName) { expect( expected.commandName, - `expected ${prefix}.commandName to equal ${expected.commandName} but received ${actual.commandName + `expected ${prefix}.commandName to equal ${expected.commandName} but received ${ + actual.commandName }` ).to.equal(actual.commandName); } if (expected.databaseName) { expect( expected.databaseName, - `expected ${prefix}.databaseName to equal ${expected.databaseName} but received ${actual.databaseName + `expected ${prefix}.databaseName to equal ${expected.databaseName} but received ${ + actual.databaseName }` ).to.equal(actual.databaseName); } @@ -508,14 +512,16 @@ function compareCommandFailedEvents( if (expected.commandName) { expect( expected.commandName, - `expected ${prefix}.commandName to equal ${expected.commandName} but received ${actual.commandName + `expected ${prefix}.commandName to equal ${expected.commandName} but received ${ + actual.commandName }` ).to.equal(actual.commandName); } if (expected.databaseName) { expect( expected.databaseName, - `expected ${prefix}.databaseName to equal ${expected.databaseName} but received ${actual.databaseName + `expected ${prefix}.databaseName to equal ${expected.databaseName} but received ${ + actual.databaseName }` ).to.equal(actual.databaseName); } From 07de741aca22f400237926252cf65ecf3edd813b Mon Sep 17 00:00:00 2001 From: bailey Date: Mon, 21 Jul 2025 10:09:21 -0600 Subject: [PATCH 7/7] make ClientSideEncryptionFilter use instance fields, not static fields --- .../filters/client_encryption_filter.ts | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/test/tools/runner/filters/client_encryption_filter.ts b/test/tools/runner/filters/client_encryption_filter.ts index 49347cde63a..17c56190154 100644 --- a/test/tools/runner/filters/client_encryption_filter.ts +++ b/test/tools/runner/filters/client_encryption_filter.ts @@ -21,16 +21,16 @@ import { Filter } from './filter'; */ export class ClientSideEncryptionFilter extends Filter { - enabled: boolean; - static version = null; - static libmongocrypt: string | 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; } catch (failedToGetFLELib) { @@ -39,12 +39,14 @@ export class ClientSideEncryptionFilter extends Filter { } } - 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); @@ -52,8 +54,8 @@ export class ClientSideEncryptionFilter extends Filter { context.clientSideEncryption = { enabled: this.enabled, mongodbClientEncryption, - version: ClientSideEncryptionFilter.version, - libmongocrypt: ClientSideEncryptionFilter.libmongocrypt + version: this.version, + libmongocrypt: this.libmongocrypt }; } @@ -73,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}`; } }