From 8edcab78969d049ad60f5fd999035351b2014587 Mon Sep 17 00:00:00 2001 From: Sergey Zelenov Date: Fri, 3 Jul 2026 13:51:40 +0200 Subject: [PATCH] feat(NODE-7638): add QE substring query type Promote QE substring string queries from Technical Preview: - add `'substring'` queryType, mark `'substringPreview'` @deprecated - drop @experimental note from StringQueryOptions.substring - bump mongodb-client-encryption to ^7.2.0 for libmongocrypt 1.20.0 - sync DRIVERS-3540 spec tests (specifications#1957): QE-Text-substring unified tests, updated QE-Text-substringPreview, and substring/ substring-preview encryptedFields data - prose test 27 Cases 5-6: run both substring (server 9.0+, libmongocrypt 1.20.0+) and substringPreview (server pre-9.0) variants mongodb-client-encryption 7.2.0 is not yet released (shipped via mongodb-js/mongodb-client-encryption#135); package-lock.json and CI installs will resolve once it is published. --- package.json | 4 +- .../client_encryption.ts | 6 +- ...e_encryption.prose.27.text_queries.test.ts | 252 +++++++--- .../encryptedFields-substring-preview.json | 33 ++ .../etc/data/encryptedFields-substring.json | 10 +- .../tests/unified/QE-Text-substring.json | 467 ++++++++++++++++++ .../tests/unified/QE-Text-substring.yml | 374 ++++++++++++++ .../unified/QE-Text-substringPreview.json | 87 +--- .../unified/QE-Text-substringPreview.yml | 101 +--- 9 files changed, 1065 insertions(+), 269 deletions(-) create mode 100644 test/spec/client-side-encryption/etc/data/encryptedFields-substring-preview.json create mode 100644 test/spec/client-side-encryption/tests/unified/QE-Text-substring.json create mode 100644 test/spec/client-side-encryption/tests/unified/QE-Text-substring.yml diff --git a/package.json b/package.json index 519073df79..739f110b3a 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@mongodb-js/zstd": "^7.0.0", "gcp-metadata": "^7.0.1", "kerberos": "^7.0.0", - "mongodb-client-encryption": "^7.1.0", + "mongodb-client-encryption": "^7.2.0", "snappy": "^7.3.2", "socks": "^2.8.6" }, @@ -96,7 +96,7 @@ "js-yaml": "^4.1.0", "mocha": "^11.7.5", "mocha-sinon": "^2.1.2", - "mongodb-client-encryption": "^7.1.0", + "mongodb-client-encryption": "^7.2.0", "nyc": "^17.1.0", "prettier": "^3.6.2", "semver": "^7.7.2", diff --git a/src/client-side-encryption/client_encryption.ts b/src/client-side-encryption/client_encryption.ts index 79d67559e4..19f6c8fc26 100644 --- a/src/client-side-encryption/client_encryption.ts +++ b/src/client-side-encryption/client_encryption.ts @@ -849,11 +849,12 @@ export interface ClientEncryptionEncryptOptions { | 'range' | 'prefix' | 'suffix' + | 'substring' /** @deprecated Use `'prefix'` instead. */ | 'prefixPreview' /** @deprecated Use `'suffix'` instead. */ | 'suffixPreview' - /** @experimental Public Technical Preview: `substringPreview` is an experimental feature and may break at any time. */ + /** @deprecated Use `'substring'` instead. */ | 'substringPreview'; /** The index options for a Queryable Encryption field supporting "range" queries.*/ @@ -895,9 +896,6 @@ export interface StringQueryOptions { strMinQueryLength: Int32 | number; }; - /** - * @experimental Public Technical Preview: `substring` is an experimental feature and may break at any time. - */ substring?: { /** The maximum allowed length to insert. */ strMaxLength: Int32 | number; diff --git a/test/integration/client-side-encryption/client_side_encryption.prose.27.text_queries.test.ts b/test/integration/client-side-encryption/client_side_encryption.prose.27.text_queries.test.ts index b307bb489c..afabffef8e 100644 --- a/test/integration/client-side-encryption/client_side_encryption.prose.27.text_queries.test.ts +++ b/test/integration/client-side-encryption/client_side_encryption.prose.27.text_queries.test.ts @@ -10,7 +10,7 @@ import * as semver from 'semver'; import { getCSFLEKMSProviders } from '../../csfle-kms-providers'; import { ClientEncryption, type MongoClient, MongoDBCollectionNamespace } from '../../mongodb'; -// Cases 1-4: prefix/suffix GA requires server 9.0+ (SERVER-123416) and libmongocrypt 1.19.0+ (MONGOCRYPT-870). +// Cases 1-4: prefix/suffix require server 9.0+ (SERVER-123416) and libmongocrypt 1.19.0+ (MONGOCRYPT-870). const metadata: MongoDBMetadataUI = { requires: { clientSideEncryption: '>=6.4.0', @@ -30,8 +30,18 @@ const metadataPreview: MongoDBMetadataUI = { } }; -// TODO(NODE-7623): substringPreview contention validation broken on MongoDB 9.0+ (SERVER-91887). +// Cases 5-6: substring requires server 9.0+ (SERVER-123416) and libmongocrypt 1.20.0+. const metadataSubstring: MongoDBMetadataUI = { + requires: { + clientSideEncryption: '>=6.4.0', + mongodb: '>=9.0.0', + topology: '!single', + libmongocrypt: '>=1.20.0' + } +}; + +// Cases 5-6 preview: substringPreview removed in server 9.0.0 (SERVER-123416). +const metadataSubstringPreview: MongoDBMetadataUI = { requires: { clientSideEncryption: '>=6.4.0', mongodb: '>=8.2.0 <9.0.0', @@ -91,11 +101,20 @@ describe('27. String Explicit Encryption', function () { } // - `db.substring` using the `encryptedFields` option set to the contents of - // encryptedFields-substring.json - await dropAndCreateCollection( - 'db.substring', - await loadFLEDataFile('encryptedFields-substring.json') - ); + // encryptedFields-substring.json. This step requires server 9.0.0+. + // - `db.substring-preview` using the `encryptedFields` option set to the contents of + // encryptedFields-substring-preview.json. This step requires server pre-9.0.0. + if (isServer9OrAbove) { + await dropAndCreateCollection( + 'db.substring', + await loadFLEDataFile('encryptedFields-substring.json') + ); + } else { + await dropAndCreateCollection( + 'db.substring-preview', + await loadFLEDataFile('encryptedFields-substring-preview.json') + ); + } // Load the file key1-document.json as `key1Document`. keyDocument1 = await loadFLEDataFile('keys/key1-document.json'); @@ -199,13 +218,13 @@ describe('27. String Explicit Encryption', function () { // diacriticSensitive: true, // substring: SubstringOpts { // strMaxLength: 10, - // strMaxQueryLength: 10, + // strMaxQueryLength: 6, // strMinQueryLength: 2, // } // }, // } - // Use `explicitEncryptedClient` to insert the following document into `db.substring` with - // majority write concern: + // Use `explicitEncryptedClient` to insert the following document into `db.substring` + // (if created) and `db.substring-preview` (if created) with majority write concern: // { "_id": 0, "encryptedText": } { const encryptedText = await clientEncryption.encrypt('foobarbaz', { @@ -215,13 +234,20 @@ describe('27. String Explicit Encryption', function () { stringOptions: { caseSensitive: true, diacriticSensitive: true, - substring: { strMaxLength: 10, strMaxQueryLength: 10, strMinQueryLength: 2 } + substring: { strMaxLength: 10, strMaxQueryLength: 6, strMinQueryLength: 2 } } }); - await explicitEncryptedClient - .db('db') - .collection<{ _id: number; encryptedText: Binary }>('substring') - .insertOne({ _id: 0, encryptedText }, { writeConcern: { w: 'majority' } }); + if (isServer9OrAbove) { + await explicitEncryptedClient + .db('db') + .collection<{ _id: number; encryptedText: Binary }>('substring') + .insertOne({ _id: 0, encryptedText }, { writeConcern: { w: 'majority' } }); + } else { + await explicitEncryptedClient + .db('db') + .collection<{ _id: number; encryptedText: Binary }>('substring-preview') + .insertOne({ _id: 0, encryptedText }, { writeConcern: { w: 'majority' } }); + } } }); @@ -254,7 +280,7 @@ describe('27. String Explicit Encryption', function () { // } // }, // } - it('GA', metadata, async function () { + it('prefix', metadata, async function () { // Use `clientEncryption.encrypt()` to encrypt the string `"foo"` with the following `EncryptOpts`: const encryptedFoo = await clientEncryption.encrypt('foo', { keyId: keyId1, @@ -303,7 +329,7 @@ describe('27. String Explicit Encryption', function () { // } // }, // } - it('preview', metadataPreview, async function () { + it('prefixPreview', metadataPreview, async function () { // Use `clientEncryption.encrypt()` to encrypt the string `"foo"` with the following `EncryptOpts`: const encryptedFoo = await clientEncryption.encrypt('foo', { keyId: keyId1, @@ -359,7 +385,7 @@ describe('27. String Explicit Encryption', function () { // } // }, // } - it('GA', metadata, async function () { + it('suffix', metadata, async function () { // Use `clientEncryption.encrypt()` to encrypt the string `"baz"` with the following `EncryptOpts`: const encryptedBaz = await clientEncryption.encrypt('baz', { keyId: keyId1, @@ -408,7 +434,7 @@ describe('27. String Explicit Encryption', function () { // } // }, // } - it('preview', metadataPreview, async function () { + it('suffixPreview', metadataPreview, async function () { // Use `clientEncryption.encrypt()` to encrypt the string `"baz"` with the following `EncryptOpts`: const encryptedBaz = await clientEncryption.encrypt('baz', { keyId: keyId1, @@ -464,7 +490,7 @@ describe('27. String Explicit Encryption', function () { // } // }, // } - it('GA', metadata, async function () { + it('prefix', metadata, async function () { // Use `clientEncryption.encrypt()` to encrypt the string `"baz"` with the following `EncryptOpts`: const encryptedBaz = await clientEncryption.encrypt('baz', { keyId: keyId1, @@ -505,7 +531,7 @@ describe('27. String Explicit Encryption', function () { // } // }, // } - it('preview', metadataPreview, async function () { + it('prefixPreview', metadataPreview, async function () { // Use `clientEncryption.encrypt()` to encrypt the string `"baz"` with the following `EncryptOpts`: const encryptedBaz = await clientEncryption.encrypt('baz', { keyId: keyId1, @@ -554,7 +580,7 @@ describe('27. String Explicit Encryption', function () { // } // }, // } - it('GA', metadata, async function () { + it('suffix', metadata, async function () { // Use `clientEncryption.encrypt()` to encrypt the string `"foo"` with the following `EncryptOpts`: const encryptedFoo = await clientEncryption.encrypt('foo', { keyId: keyId1, @@ -595,7 +621,7 @@ describe('27. String Explicit Encryption', function () { // } // }, // } - it('preview', metadataPreview, async function () { + it('suffixPreview', metadataPreview, async function () { // Use `clientEncryption.encrypt()` to encrypt the string `"foo"` with the following `EncryptOpts`: const encryptedFoo = await clientEncryption.encrypt('foo', { keyId: keyId1, @@ -623,90 +649,168 @@ describe('27. String Explicit Encryption', function () { }); }); - it('Case 5: can find a document by substring', metadataSubstring, async function () { - // Use `clientEncryption.encrypt()` to encrypt the string `"bar"` with the following `EncryptOpts`: + // Run this case multiple times with the following sets of parameters: + // - `queryType=substring` and `collection=substring` + // - Require server 9.0.0+ and libmongocrypt 1.20.0+. + // - `queryType=substringPreview` and `collection=substring-preview` + // - Require server pre-9.0.0 and libmongocrypt 1.18.1+. + context('Case 5: can find a document by substring', function () { + // `queryType=substring` and `collection=substring` // class EncryptOpts { // keyId : , // algorithm: "String", - // queryType: "substringPreview", + // queryType: "substring", // contentionFactor: 0, // stringOpts: StringOpts { // caseSensitive: true, // diacriticSensitive: true, // substring: SubstringOpts { // strMaxLength: 10, - // strMaxQueryLength: 10, + // strMaxQueryLength: 6, // strMinQueryLength: 2, // } // }, // } - const encryptedBar = await clientEncryption.encrypt('bar', { - keyId: keyId1, - algorithm: 'String', - queryType: 'substringPreview', - contentionFactor: 0, - stringOptions: { - caseSensitive: true, - diacriticSensitive: true, - substring: { strMaxLength: 10, strMaxQueryLength: 10, strMinQueryLength: 2 } - } + it('substring', metadataSubstring, async function () { + // Use `clientEncryption.encrypt()` to encrypt the string `"bar"` with the following `EncryptOpts`: + const encryptedBar = await clientEncryption.encrypt('bar', { + keyId: keyId1, + algorithm: 'String', + queryType: 'substring', + contentionFactor: 0, + stringOptions: { + caseSensitive: true, + diacriticSensitive: true, + substring: { strMaxLength: 10, strMaxQueryLength: 6, strMinQueryLength: 2 } + } + }); + + // Use `explicitEncryptedClient` to run a "find" operation on the `db.` collection + // with the following filter: + // { $expr: { $encStrContains: {input: '$encryptedText', substring: } } } + const filter = { + $expr: { $encStrContains: { input: '$encryptedText', substring: encryptedBar } } + }; + const { __safeContent__, ...result } = await explicitEncryptedClient + .db('db') + .collection<{ _id: number; encryptedText: Binary; __safeContent__: unknown }>('substring') + .findOne(filter); + + // Assert the following document is returned: + // { "_id": 0, "encryptedText": "foobarbaz" } + expect(result).to.deep.equal({ _id: 0, encryptedText: 'foobarbaz' }); }); - // Use `explicitEncryptedClient` to run a "find" operation on the `db.substring` collection - // with the following filter: - // { $expr: { $encStrContains: {input: '$encryptedText', substring: } } } - const filter = { - $expr: { $encStrContains: { input: '$encryptedText', substring: encryptedBar } } - }; - const { __safeContent__, ...result } = await explicitEncryptedClient - .db('db') - .collection<{ _id: number; encryptedText: Binary; __safeContent__: unknown }>('substring') - .findOne(filter); - - // Assert the following document is returned: - // { "_id": 0, "encryptedText": "foobarbaz" } - expect(result).to.deep.equal({ _id: 0, encryptedText: 'foobarbaz' }); + // `queryType=substringPreview` and `collection=substring-preview` + it('substringPreview', metadataSubstringPreview, async function () { + // Use `clientEncryption.encrypt()` to encrypt the string `"bar"` with the following `EncryptOpts`: + const encryptedBar = await clientEncryption.encrypt('bar', { + keyId: keyId1, + algorithm: 'String', + queryType: 'substringPreview', + contentionFactor: 0, + stringOptions: { + caseSensitive: true, + diacriticSensitive: true, + substring: { strMaxLength: 10, strMaxQueryLength: 6, strMinQueryLength: 2 } + } + }); + + // Use `explicitEncryptedClient` to run a "find" operation on the `db.` collection + // with the following filter: + // { $expr: { $encStrContains: {input: '$encryptedText', substring: } } } + const filter = { + $expr: { $encStrContains: { input: '$encryptedText', substring: encryptedBar } } + }; + const { __safeContent__, ...result } = await explicitEncryptedClient + .db('db') + .collection<{ + _id: number; + encryptedText: Binary; + __safeContent__: unknown; + }>('substring-preview') + .findOne(filter); + + // Assert the following document is returned: + // { "_id": 0, "encryptedText": "foobarbaz" } + expect(result).to.deep.equal({ _id: 0, encryptedText: 'foobarbaz' }); + }); }); - it('Case 6: assert no document found by substring', metadataSubstring, async function () { - // Use `clientEncryption.encrypt()` to encrypt the string `"qux"` with the following `EncryptOpts`: + // Run this case multiple times with the following sets of parameters: + // - `queryType=substring` and `collection=substring` + // - Require server 9.0.0+ and libmongocrypt 1.20.0+. + // - `queryType=substringPreview` and `collection=substring-preview` + // - Require server pre-9.0.0 and libmongocrypt 1.18.1+. + context('Case 6: assert no document found by substring', function () { + // `queryType=substring` and `collection=substring` // class EncryptOpts { // keyId : , // algorithm: "String", - // queryType: "substringPreview", + // queryType: "substring", // contentionFactor: 0, // stringOpts: StringOpts { // caseSensitive: true, // diacriticSensitive: true, // substring: SubstringOpts { // strMaxLength: 10, - // strMaxQueryLength: 10, + // strMaxQueryLength: 6, // strMinQueryLength: 2, // } // }, // } - const encryptedQux = await clientEncryption.encrypt('qux', { - keyId: keyId1, - algorithm: 'String', - queryType: 'substringPreview', - contentionFactor: 0, - stringOptions: { - caseSensitive: true, - diacriticSensitive: true, - substring: { strMaxLength: 10, strMaxQueryLength: 10, strMinQueryLength: 2 } - } + it('substring', metadataSubstring, async function () { + // Use `clientEncryption.encrypt()` to encrypt the string `"qux"` with the following `EncryptOpts`: + const encryptedQux = await clientEncryption.encrypt('qux', { + keyId: keyId1, + algorithm: 'String', + queryType: 'substring', + contentionFactor: 0, + stringOptions: { + caseSensitive: true, + diacriticSensitive: true, + substring: { strMaxLength: 10, strMaxQueryLength: 6, strMinQueryLength: 2 } + } + }); + + // Use `explicitEncryptedClient` to run a "find" operation on the `db.` collection + // with the following filter: + // { $expr: { $encStrContains: {input: '$encryptedText', substring: } } } + const filter = { + $expr: { $encStrContains: { input: '$encryptedText', substring: encryptedQux } } + }; + + // Assert that no documents are returned. + expect(await explicitEncryptedClient.db('db').collection('substring').findOne(filter)).to.be + .null; }); - // Use `explicitEncryptedClient` to run a "find" operation on the `db.substring` collection - // with the following filter: - // { $expr: { $encStrContains: {input: '$encryptedText', substring: } } } - const filter = { - $expr: { $encStrContains: { input: '$encryptedText', substring: encryptedQux } } - }; + // `queryType=substringPreview` and `collection=substring-preview` + it('substringPreview', metadataSubstringPreview, async function () { + // Use `clientEncryption.encrypt()` to encrypt the string `"qux"` with the following `EncryptOpts`: + const encryptedQux = await clientEncryption.encrypt('qux', { + keyId: keyId1, + algorithm: 'String', + queryType: 'substringPreview', + contentionFactor: 0, + stringOptions: { + caseSensitive: true, + diacriticSensitive: true, + substring: { strMaxLength: 10, strMaxQueryLength: 6, strMinQueryLength: 2 } + } + }); + + // Use `explicitEncryptedClient` to run a "find" operation on the `db.` collection + // with the following filter: + // { $expr: { $encStrContains: {input: '$encryptedText', substring: } } } + const filter = { + $expr: { $encStrContains: { input: '$encryptedText', substring: encryptedQux } } + }; - // Assert that no documents are returned. - expect(await explicitEncryptedClient.db('db').collection('substring').findOne(filter)).to.be - .null; + // Assert that no documents are returned. + expect(await explicitEncryptedClient.db('db').collection('substring-preview').findOne(filter)) + .to.be.null; + }); }); it('Case 7: assert contentionFactor is required', metadata, async function () { diff --git a/test/spec/client-side-encryption/etc/data/encryptedFields-substring-preview.json b/test/spec/client-side-encryption/etc/data/encryptedFields-substring-preview.json new file mode 100644 index 0000000000..a519edc627 --- /dev/null +++ b/test/spec/client-side-encryption/etc/data/encryptedFields-substring-preview.json @@ -0,0 +1,33 @@ +{ + "fields": [ + { + "keyId": { + "$binary": { + "base64": "EjRWeBI0mHYSNBI0VniQEg==", + "subType": "04" + } + }, + "path": "encryptedText", + "bsonType": "string", + "queries": [ + { + "queryType": "substringPreview", + "strMaxLength": { + "$numberInt": "10" + }, + "strMinQueryLength": { + "$numberInt": "2" + }, + "strMaxQueryLength": { + "$numberInt": "6" + }, + "contention": { + "$numberLong": "0" + }, + "caseSensitive": true, + "diacriticSensitive": true + } + ] + } + ] +} diff --git a/test/spec/client-side-encryption/etc/data/encryptedFields-substring.json b/test/spec/client-side-encryption/etc/data/encryptedFields-substring.json index d321a32c5c..5e80f4fc95 100644 --- a/test/spec/client-side-encryption/etc/data/encryptedFields-substring.json +++ b/test/spec/client-side-encryption/etc/data/encryptedFields-substring.json @@ -1,6 +1,6 @@ { - "fields": [ - { + "fields": [ + { "keyId": { "$binary": { "base64": "EjRWeBI0mHYSNBI0VniQEg==", @@ -11,7 +11,7 @@ "bsonType": "string", "queries": [ { - "queryType": "substringPreview", + "queryType": "substring", "strMaxLength": { "$numberInt": "10" }, @@ -19,7 +19,7 @@ "$numberInt": "2" }, "strMaxQueryLength": { - "$numberInt": "10" + "$numberInt": "6" }, "contention": { "$numberLong": "0" @@ -29,5 +29,5 @@ } ] } - ] + ] } diff --git a/test/spec/client-side-encryption/tests/unified/QE-Text-substring.json b/test/spec/client-side-encryption/tests/unified/QE-Text-substring.json new file mode 100644 index 0000000000..f42a297278 --- /dev/null +++ b/test/spec/client-side-encryption/tests/unified/QE-Text-substring.json @@ -0,0 +1,467 @@ +{ + "description": "QE-Text-substring", + "schemaVersion": "1.25", + "runOnRequirements": [ + { + "minServerVersion": "9.0.0", + "topologies": [ + "replicaset", + "sharded", + "load-balanced" + ], + "csfle": { + "minLibmongocryptVersion": "1.20.0" + } + } + ], + "createEntities": [ + { + "client": { + "id": "client", + "autoEncryptOpts": { + "keyVaultNamespace": "keyvault.datakeys", + "kmsProviders": { + "local": { + "key": "Mng0NCt4ZHVUYUJCa1kxNkVyNUR1QURhZ2h2UzR2d2RrZzh0cFBwM3R6NmdWMDFBMUN3YkQ5aXRRMkhGRGdQV09wOGVNYUMxT2k3NjZKelhaQmRCZGJkTXVyZG9uSjFk" + } + } + }, + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "db", + "client": "client", + "databaseName": "db" + } + }, + { + "collection": { + "id": "coll", + "database": "db", + "collectionName": "coll" + } + } + ], + "initialData": [ + { + "databaseName": "keyvault", + "collectionName": "datakeys", + "documents": [ + { + "_id": { + "$binary": { + "base64": "q83vqxI0mHYSNBI0VniQEg==", + "subType": "04" + } + }, + "keyMaterial": { + "$binary": { + "base64": "HBk9BWihXExNDvTp1lUxOuxuZK2Pe2ZdVdlsxPEBkiO1bS4mG5NNDsQ7zVxJAH8BtdOYp72Ku4Y3nwc0BUpIKsvAKX4eYXtlhv5zUQxWdeNFhg9qK7qb8nqhnnLeT0f25jFSqzWJoT379hfwDeu0bebJHr35QrJ8myZdPMTEDYF08QYQ48ShRBli0S+QzBHHAQiM2iJNr4svg2WR8JSeWQ==", + "subType": "00" + } + }, + "creationDate": { + "$date": { + "$numberLong": "1648914851981" + } + }, + "updateDate": { + "$date": { + "$numberLong": "1648914851981" + } + }, + "status": { + "$numberInt": "0" + }, + "masterKey": { + "provider": "local" + } + } + ] + }, + { + "databaseName": "db", + "collectionName": "coll", + "documents": [], + "createOptions": { + "encryptedFields": { + "fields": [ + { + "keyId": { + "$binary": { + "base64": "q83vqxI0mHYSNBI0VniQEg==", + "subType": "04" + } + }, + "path": "encryptedText", + "bsonType": "string", + "queries": [ + { + "queryType": "substring", + "contention": { + "$numberLong": "0" + }, + "strMinQueryLength": { + "$numberLong": "3" + }, + "strMaxQueryLength": { + "$numberLong": "6" + }, + "strMaxLength": { + "$numberLong": "20" + }, + "caseSensitive": true, + "diacriticSensitive": true + } + ] + } + ] + } + } + } + ], + "tests": [ + { + "description": "Insert QE substring", + "operations": [ + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 1, + "encryptedText": "foobar" + } + }, + "object": "coll" + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "listCollections": 1, + "filter": { + "name": "coll" + } + }, + "commandName": "listCollections" + } + }, + { + "commandStartedEvent": { + "command": { + "find": "datakeys", + "filter": { + "$or": [ + { + "_id": { + "$in": [ + { + "$binary": { + "base64": "q83vqxI0mHYSNBI0VniQEg==", + "subType": "04" + } + } + ] + } + }, + { + "keyAltNames": { + "$in": [] + } + } + ] + }, + "$db": "keyvault", + "readConcern": { + "level": "majority" + } + }, + "commandName": "find" + } + }, + { + "commandStartedEvent": { + "command": { + "insert": "coll", + "documents": [ + { + "_id": 1, + "encryptedText": { + "$$type": "binData" + } + } + ], + "ordered": true + }, + "commandName": "insert" + } + } + ] + } + ] + }, + { + "description": "Query with matching $encStrContains", + "operations": [ + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 1, + "encryptedText": "foobar" + } + }, + "object": "coll" + }, + { + "name": "find", + "arguments": { + "filter": { + "$expr": { + "$encStrContains": { + "input": "$encryptedText", + "substring": "oba" + } + } + } + }, + "object": "coll", + "expectResult": [ + { + "_id": { + "$numberInt": "1" + }, + "encryptedText": "foobar", + "__safeContent__": [ + { + "$binary": { + "base64": "wpaMBVDjL4bHf9EtSP52PJFzyNn1R19+iNI/hWtvzdk=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "IpY3x/jjm8j/74jAdUhgxdM5hk68zR0zv/lTKm/72Vg=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "G+ky260C6QiOfIxKz14FmaMbAxvui1BKJO/TnLOHlGk=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "7dv3gAKe9vwJMZmpB40pRCwRTmc7ds9UkGhxH8j084E=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "o0V+Efn6x8XQdE80F1tztNaT3qxHjcsd9DOQ47BtmQk=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "sJvrCjyVot7PIZFsdRehWFANKAj6fmBaj3FLbz/dZLE=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "e98auxFmu02h5MfBIARk29MI7hSmvN3F9DaQ0xjqoEM=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "US83krGNov/ezL6IhsY5eEOCxv1xUPDIEL/nmY0IKi0=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "P2Aq5+OHZPG0CWIdmZvWq9c/18ZKVYW3vbxd+WU/TXU=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "8AdPRPnSzcd5uhq4TZfNvNeF0XjLNVwAsJJMTtktw84=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "9O6u/G51I4ZHFLhL4ZLuudbr0s202A2QnPfThmOXPhI=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "N7AjYVyVlv6+lVSTM+cIxRL3SMgs3G5LgxSs+jrgDkI=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "RbGF7dQbPGYQFd9DDO1hPz1UlLOJ77FAC6NsjGwJeos=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "m7srHMgKm6kZwsNx8rc45pmw0/9Qro6xuQ8lZS3+RYk=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "K75CNU3JyKFqZWPiIsVi4+n7DhYmcPl/nEhQ3d88mVI=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "c7bwGpUZc/7JzEnMS7qQ/TPuXZyrmMihFaAV6zIqbZc=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "rDvEdUgEk8u4Srt3ETokWs2FXcnyJaRGQ+NbkFwi2rQ=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "VcdZj9zfveRBRlpCR2OYWau2+GokOFb73TE3gpElNiU=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "eOa9o2xfA6OgkbYUxd6wQJicaeN6guhy2V66W3ALsaA=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "1xGkJh+um70XiRd8lKLDtyHgDqrf7/59Mg7X0+KZh8k=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "OSvllqHxycbcZN4phR6NDujY3ttA59o7nQJ6V9eJpX0=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "ZTX1pyk8Vdw0BSbJx7GeJNcQf3tGKxbrrNSTqBqUWkg=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "cn7V05zb5iXwYrePGMHztC+GRq+Tj8IMpRDraauPhSE=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "E9bV9KyrZxHJSUmMg0HrDK4gGN+75ruelAnrM6hXQgY=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "WrssTNmdgXoTGpbaF0JLRCGH6cDQuz1XEFNTy98nrb0=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "jZmyOJP35dsxQ/OY5U4ISpVRIYr8iedNfcwZiKt29Qc=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "d2mocORMbX9MX+/itAW8r1kxVw2/uii4vzXtc+2CIRQ=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "JBnJy58eRPhDo3DuZvsHbvQDiHXxdtAx1Eif66k5SfA=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "OjbDulC8s62v0pgweBSsQqtJjJBwH5JinfJpj7nVr+A=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "85i7KT2GP9nSda3Gsil5LKubhq0LDtc22pxBxHpR+nE=", + "subType": "00" + } + }, + { + "$binary": { + "base64": "u9Fvsclwrs9lwIcMPV/fMZD7L3d5anSfJQVjQb9mgLg=", + "subType": "00" + } + } + ] + } + ] + } + ] + }, + { + "description": "Query with non-matching $encStrContains", + "operations": [ + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 1, + "encryptedText": "foobar" + } + }, + "object": "coll" + }, + { + "name": "find", + "arguments": { + "filter": { + "$expr": { + "$encStrContains": { + "input": "$encryptedText", + "substring": "blah" + } + } + } + }, + "object": "coll", + "expectResult": [] + } + ] + } + ] +} diff --git a/test/spec/client-side-encryption/tests/unified/QE-Text-substring.yml b/test/spec/client-side-encryption/tests/unified/QE-Text-substring.yml new file mode 100644 index 0000000000..baac965459 --- /dev/null +++ b/test/spec/client-side-encryption/tests/unified/QE-Text-substring.yml @@ -0,0 +1,374 @@ +description: QE-Text-substring +schemaVersion: "1.25" +runOnRequirements: + - minServerVersion: "9.0.0" # Server 9.0.0 adds stable support for QE text substring queries. + topologies: ["replicaset", "sharded", "load-balanced"] # QE does not support standalone. + csfle: + minLibmongocryptVersion: 1.20.0 # For MONOGCRYPT-936. +createEntities: + - client: + id: &client "client" + autoEncryptOpts: + keyVaultNamespace: keyvault.datakeys + kmsProviders: + local: + key: Mng0NCt4ZHVUYUJCa1kxNkVyNUR1QURhZ2h2UzR2d2RrZzh0cFBwM3R6NmdWMDFBMUN3YkQ5aXRRMkhGRGdQV09wOGVNYUMxT2k3NjZKelhaQmRCZGJkTXVyZG9uSjFk + observeEvents: + - commandStartedEvent + - database: + id: &db "db" + client: *client + databaseName: *db + - collection: + id: &coll "coll" + database: *db + collectionName: *coll +initialData: + # Insert data encryption key: + - databaseName: keyvault + collectionName: datakeys + documents: + [ + { + "_id": &keyid { "$binary": { "base64": "q83vqxI0mHYSNBI0VniQEg==", "subType": "04" } }, + "keyMaterial": + { + "$binary": + { + "base64": "HBk9BWihXExNDvTp1lUxOuxuZK2Pe2ZdVdlsxPEBkiO1bS4mG5NNDsQ7zVxJAH8BtdOYp72Ku4Y3nwc0BUpIKsvAKX4eYXtlhv5zUQxWdeNFhg9qK7qb8nqhnnLeT0f25jFSqzWJoT379hfwDeu0bebJHr35QrJ8myZdPMTEDYF08QYQ48ShRBli0S+QzBHHAQiM2iJNr4svg2WR8JSeWQ==", + "subType": "00", + }, + }, + "creationDate": { "$date": { "$numberLong": "1648914851981" } }, + "updateDate": { "$date": { "$numberLong": "1648914851981" } }, + "status": { "$numberInt": "0" }, + "masterKey": { "provider": "local" }, + }, + ] + # Create encrypted collection: + - databaseName: *db + collectionName: *coll + documents: [] + createOptions: + encryptedFields: + { + "fields": + [ + { + "keyId": *keyid, + "path": "encryptedText", + "bsonType": "string", + "queries": [ + # Use zero contention for deterministic __safeContent__: + { + "queryType": "substring", + "contention": { "$numberLong": "0" }, + "strMinQueryLength": { "$numberLong": "3" }, + "strMaxQueryLength": { "$numberLong": "6" }, + "strMaxLength": { "$numberLong": "20" }, + "caseSensitive": true, + "diacriticSensitive": true, + }, + ], + }, + ], + } +tests: + - description: "Insert QE substring" + operations: + - name: insertOne + arguments: + document: { _id: 1, encryptedText: "foobar" } + object: *coll + expectEvents: + - client: "client" + events: + - commandStartedEvent: + command: + listCollections: 1 + filter: + name: *coll + commandName: listCollections + - commandStartedEvent: + command: + find: datakeys + filter: + { + "$or": + [ + "_id": { "$in": [ *keyid ] }, + "keyAltNames": { "$in": [] }, + ], + } + $db: keyvault + readConcern: { level: "majority" } + commandName: find + - commandStartedEvent: + command: + insert: *coll + documents: + - { "_id": 1, "encryptedText": { $$type: "binData" } } # Sends encrypted payload + ordered: true + commandName: insert + - description: "Query with matching $encStrContains" + operations: + - name: insertOne + arguments: + document: { _id: 1, encryptedText: "foobar" } + object: *coll + - name: find + arguments: + filter: + { + $expr: + { + $encStrContains: + { input: "$encryptedText", substring: "oba" }, + }, + } + object: *coll + expectResult: + [ + { + "_id": { "$numberInt": "1" }, + "encryptedText": "foobar", + "__safeContent__": + [ + { + "$binary": + { + "base64": "wpaMBVDjL4bHf9EtSP52PJFzyNn1R19+iNI/hWtvzdk=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "IpY3x/jjm8j/74jAdUhgxdM5hk68zR0zv/lTKm/72Vg=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "G+ky260C6QiOfIxKz14FmaMbAxvui1BKJO/TnLOHlGk=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "7dv3gAKe9vwJMZmpB40pRCwRTmc7ds9UkGhxH8j084E=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "o0V+Efn6x8XQdE80F1tztNaT3qxHjcsd9DOQ47BtmQk=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "sJvrCjyVot7PIZFsdRehWFANKAj6fmBaj3FLbz/dZLE=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "e98auxFmu02h5MfBIARk29MI7hSmvN3F9DaQ0xjqoEM=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "US83krGNov/ezL6IhsY5eEOCxv1xUPDIEL/nmY0IKi0=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "P2Aq5+OHZPG0CWIdmZvWq9c/18ZKVYW3vbxd+WU/TXU=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "8AdPRPnSzcd5uhq4TZfNvNeF0XjLNVwAsJJMTtktw84=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "9O6u/G51I4ZHFLhL4ZLuudbr0s202A2QnPfThmOXPhI=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "N7AjYVyVlv6+lVSTM+cIxRL3SMgs3G5LgxSs+jrgDkI=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "RbGF7dQbPGYQFd9DDO1hPz1UlLOJ77FAC6NsjGwJeos=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "m7srHMgKm6kZwsNx8rc45pmw0/9Qro6xuQ8lZS3+RYk=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "K75CNU3JyKFqZWPiIsVi4+n7DhYmcPl/nEhQ3d88mVI=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "c7bwGpUZc/7JzEnMS7qQ/TPuXZyrmMihFaAV6zIqbZc=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "rDvEdUgEk8u4Srt3ETokWs2FXcnyJaRGQ+NbkFwi2rQ=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "VcdZj9zfveRBRlpCR2OYWau2+GokOFb73TE3gpElNiU=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "eOa9o2xfA6OgkbYUxd6wQJicaeN6guhy2V66W3ALsaA=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "1xGkJh+um70XiRd8lKLDtyHgDqrf7/59Mg7X0+KZh8k=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "OSvllqHxycbcZN4phR6NDujY3ttA59o7nQJ6V9eJpX0=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "ZTX1pyk8Vdw0BSbJx7GeJNcQf3tGKxbrrNSTqBqUWkg=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "cn7V05zb5iXwYrePGMHztC+GRq+Tj8IMpRDraauPhSE=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "E9bV9KyrZxHJSUmMg0HrDK4gGN+75ruelAnrM6hXQgY=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "WrssTNmdgXoTGpbaF0JLRCGH6cDQuz1XEFNTy98nrb0=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "jZmyOJP35dsxQ/OY5U4ISpVRIYr8iedNfcwZiKt29Qc=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "d2mocORMbX9MX+/itAW8r1kxVw2/uii4vzXtc+2CIRQ=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "JBnJy58eRPhDo3DuZvsHbvQDiHXxdtAx1Eif66k5SfA=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "OjbDulC8s62v0pgweBSsQqtJjJBwH5JinfJpj7nVr+A=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "85i7KT2GP9nSda3Gsil5LKubhq0LDtc22pxBxHpR+nE=", + "subType": "00", + }, + }, + { + "$binary": + { + "base64": "u9Fvsclwrs9lwIcMPV/fMZD7L3d5anSfJQVjQb9mgLg=", + "subType": "00", + }, + }, + ], + }, + ] + + - description: "Query with non-matching $encStrContains" + operations: + - name: insertOne + arguments: + document: { _id: 1, encryptedText: "foobar" } + object: *coll + - name: find + arguments: + filter: + { + $expr: + { + $encStrContains: { input: "$encryptedText", substring: "blah" }, + }, + } + object: *coll + expectResult: [] diff --git a/test/spec/client-side-encryption/tests/unified/QE-Text-substringPreview.json b/test/spec/client-side-encryption/tests/unified/QE-Text-substringPreview.json index 7787194fc6..1df3170958 100644 --- a/test/spec/client-side-encryption/tests/unified/QE-Text-substringPreview.json +++ b/test/spec/client-side-encryption/tests/unified/QE-Text-substringPreview.json @@ -4,6 +4,7 @@ "runOnRequirements": [ { "minServerVersion": "8.2.0", + "maxServerVersion": "8.99.99", "topologies": [ "replicaset", "sharded", @@ -109,7 +110,7 @@ "$numberLong": "3" }, "strMaxQueryLength": { - "$numberLong": "10" + "$numberLong": "6" }, "strMaxLength": { "$numberLong": "20" @@ -426,90 +427,6 @@ "base64": "u9Fvsclwrs9lwIcMPV/fMZD7L3d5anSfJQVjQb9mgLg=", "subType": "00" } - }, - { - "$binary": { - "base64": "LZ32ttmLJGOIw9oFaUCn3Sx5uHPTYJPSFpeGRWNqlUc=", - "subType": "00" - } - }, - { - "$binary": { - "base64": "mMsZvGEePTqtl0FJAL/jAdyWNQIlpwN61YIlZsSIZ6s=", - "subType": "00" - } - }, - { - "$binary": { - "base64": "XZcu1a/ZGsIzAl3j4MXQlLo4v2p7kvIqRHtIQYFmL6k=", - "subType": "00" - } - }, - { - "$binary": { - "base64": "Zse27LinlYCEnX6iTmJceI33mEJxFb0LdPxp0RiMOaQ=", - "subType": "00" - } - }, - { - "$binary": { - "base64": "vOv2Hgb2/sBpnX9XwFbIN6yDxhjchwlmczUf82W2tp4=", - "subType": "00" - } - }, - { - "$binary": { - "base64": "oQxZ9A6j3x5j6x1Jqw/N9tpP4rfWMjcV3y+a3PkrL7c=", - "subType": "00" - } - }, - { - "$binary": { - "base64": "/D7ew3EijyUnmT22awVFspcuyo3JChJcDeCPwpljzVM=", - "subType": "00" - } - }, - { - "$binary": { - "base64": "BEmmwqyamt9X3bcWDld61P01zquy8fBHAXq3SHAPP0M=", - "subType": "00" - } - }, - { - "$binary": { - "base64": "wygD9/kAo1KsRvtr1v+9/lvqoWdKwgh6gDHvAQfXPPk=", - "subType": "00" - } - }, - { - "$binary": { - "base64": "pRTKgF/uksrF1c1AcfSTY6ZhqBKVud1vIztQ4/36SLs=", - "subType": "00" - } - }, - { - "$binary": { - "base64": "C4iUo8oNJsjJ37BqnBgIgSQpf99X2Bb4W5MZEAmakHU=", - "subType": "00" - } - }, - { - "$binary": { - "base64": "icoE53jIq6Fu/YGKUiSUTYyZ8xdiTQY9jJiGxVJObpw=", - "subType": "00" - } - }, - { - "$binary": { - "base64": "oubCwk0V6G2RFWtcOnYDU4uUBoXBrhBRi4nZgrYj9JY=", - "subType": "00" - } - }, - { - "$binary": { - "base64": "IyqhQ9nGhzEi5YW2W6v1kGU5DY2u2qSqbM/qXdLdWVU=", - "subType": "00" - } } ] } diff --git a/test/spec/client-side-encryption/tests/unified/QE-Text-substringPreview.yml b/test/spec/client-side-encryption/tests/unified/QE-Text-substringPreview.yml index a58a77743e..d033cd4f64 100644 --- a/test/spec/client-side-encryption/tests/unified/QE-Text-substringPreview.yml +++ b/test/spec/client-side-encryption/tests/unified/QE-Text-substringPreview.yml @@ -2,6 +2,7 @@ description: QE-Text-substringPreview schemaVersion: "1.25" runOnRequirements: - minServerVersion: "8.2.0" # Server 8.2.0 adds preview support for QE text queries. + maxServerVersion: "8.99.99" # Server 9.0.0 will remove support for "substringPreview": SERVER-129158 topologies: ["replicaset", "sharded", "load-balanced"] # QE does not support standalone. csfle: minLibmongocryptVersion: 1.15.0 # For SPM-4158. @@ -64,7 +65,7 @@ initialData: "queryType": "substringPreview", "contention": { "$numberLong": "0" }, "strMinQueryLength": { "$numberLong": "3" }, - "strMaxQueryLength": { "$numberLong": "10" }, + "strMaxQueryLength": { "$numberLong": "6" }, "strMaxLength": { "$numberLong": "20" }, "caseSensitive": true, "diacriticSensitive": true, @@ -351,104 +352,6 @@ tests: "subType": "00", }, }, - { - "$binary": - { - "base64": "LZ32ttmLJGOIw9oFaUCn3Sx5uHPTYJPSFpeGRWNqlUc=", - "subType": "00", - }, - }, - { - "$binary": - { - "base64": "mMsZvGEePTqtl0FJAL/jAdyWNQIlpwN61YIlZsSIZ6s=", - "subType": "00", - }, - }, - { - "$binary": - { - "base64": "XZcu1a/ZGsIzAl3j4MXQlLo4v2p7kvIqRHtIQYFmL6k=", - "subType": "00", - }, - }, - { - "$binary": - { - "base64": "Zse27LinlYCEnX6iTmJceI33mEJxFb0LdPxp0RiMOaQ=", - "subType": "00", - }, - }, - { - "$binary": - { - "base64": "vOv2Hgb2/sBpnX9XwFbIN6yDxhjchwlmczUf82W2tp4=", - "subType": "00", - }, - }, - { - "$binary": - { - "base64": "oQxZ9A6j3x5j6x1Jqw/N9tpP4rfWMjcV3y+a3PkrL7c=", - "subType": "00", - }, - }, - { - "$binary": - { - "base64": "/D7ew3EijyUnmT22awVFspcuyo3JChJcDeCPwpljzVM=", - "subType": "00", - }, - }, - { - "$binary": - { - "base64": "BEmmwqyamt9X3bcWDld61P01zquy8fBHAXq3SHAPP0M=", - "subType": "00", - }, - }, - { - "$binary": - { - "base64": "wygD9/kAo1KsRvtr1v+9/lvqoWdKwgh6gDHvAQfXPPk=", - "subType": "00", - }, - }, - { - "$binary": - { - "base64": "pRTKgF/uksrF1c1AcfSTY6ZhqBKVud1vIztQ4/36SLs=", - "subType": "00", - }, - }, - { - "$binary": - { - "base64": "C4iUo8oNJsjJ37BqnBgIgSQpf99X2Bb4W5MZEAmakHU=", - "subType": "00", - }, - }, - { - "$binary": - { - "base64": "icoE53jIq6Fu/YGKUiSUTYyZ8xdiTQY9jJiGxVJObpw=", - "subType": "00", - }, - }, - { - "$binary": - { - "base64": "oubCwk0V6G2RFWtcOnYDU4uUBoXBrhBRi4nZgrYj9JY=", - "subType": "00", - }, - }, - { - "$binary": - { - "base64": "IyqhQ9nGhzEi5YW2W6v1kGU5DY2u2qSqbM/qXdLdWVU=", - "subType": "00", - }, - }, ], }, ]